1use alloc::boxed::Box;
66use alloc::collections::{BTreeMap, BTreeSet};
67use alloc::sync::Arc;
68use alloc::vec::Vec;
69
70use miden_protocol::account::{Account, AccountCode, AccountCodeInterface, AccountId};
71use miden_protocol::asset::{Asset, NonFungibleAsset};
72use miden_protocol::block::{BlockHeader, BlockNumber};
73use miden_protocol::errors::AssetError;
74use miden_protocol::note::{
75 Note,
76 NoteAttachments,
77 NoteDetails,
78 NoteId,
79 NoteRecipient,
80 NoteScript,
81 NoteTag,
82};
83use miden_protocol::transaction::{AccountInputs, PartialBlockchain};
84use miden_protocol::vm::MIN_STACK_DEPTH;
85use miden_protocol::{Felt, Word};
86use miden_standards::account::faucets::FungibleFaucet;
87use miden_standards::account::interface::AccountInterfaceExt;
88use miden_tx::{DataStore, NoteConsumptionChecker, TransactionExecutor};
89use tracing::info;
90
91use super::Client;
92use crate::ClientError;
93use crate::note::{NoteScreenerError, NoteUpdateTracker, StandardNote};
94use crate::rpc::domain::account::{
95 AccountStorageRequirements,
96 GetAccountRequest,
97 StorageMapFetch,
98 VaultFetch,
99};
100use crate::rpc::encryption::{TransactionEncryptionKey, seal_transaction_inputs};
101use crate::rpc::{AccountStateAt, NodeRpcClient, RpcError};
102use crate::store::data_store::{ClientDataStore, build_partial_mmr_with_paths};
103use crate::store::input_note_states::ExpectedNoteState;
104use crate::store::{
105 AccountRecord,
106 InputNoteRecord,
107 InputNoteState,
108 NoteFilter,
109 NoteRecordError,
110 OutputNoteRecord,
111 Store,
112 StoreError,
113 TransactionFilter,
114};
115use crate::sync::NoteTagRecord;
116use crate::transaction::batch::InMemoryBatchDataStore;
117
118pub mod batch;
119pub use batch::{BatchBuilder, BatchBuilderError};
120
121mod chain_anchor;
122pub use chain_anchor::{ChainAnchor, ChainAnchorError};
123
124#[cfg(feature = "dap")]
125mod dap_executor;
126mod prover;
127pub use prover::TransactionProver;
128
129mod record;
130pub use record::{
131 DiscardCause,
132 TransactionDetails,
133 TransactionRecord,
134 TransactionStatus,
135 TransactionStatusVariant,
136};
137
138mod store_update;
139pub use store_update::TransactionStoreUpdate;
140
141mod request;
142pub use request::{
143 ForeignAccount,
144 NoteArgs,
145 PaymentNoteDescription,
146 PswapTransactionData,
147 SwapTransactionData,
148 TransactionRequest,
149 TransactionRequestBuilder,
150 TransactionRequestError,
151 TransactionScriptTemplate,
152 build_fpi_script,
153};
154
155mod observer;
156pub use observer::TransactionObserver;
157
158mod result;
159pub use miden_protocol::transaction::{
162 ExecutedTransaction,
163 InputNote,
164 InputNotes,
165 OutputNote,
166 OutputNotes,
167 ProvenTransaction,
168 PublicOutputNote,
169 RawOutputNote,
170 RawOutputNotes,
171 TransactionArgs,
172 TransactionId,
173 TransactionInputs,
174 TransactionKernel,
175 TransactionScript,
176 TransactionScriptRoot,
177 TransactionSummary,
178};
179pub use miden_protocol::vm::{AdviceInputs, AdviceMap};
180pub use miden_standards::account::interface::{AccountComponentInterface, AccountInterface};
181pub use miden_standards::tx_script::{
182 ExpirationTransactionScript,
183 SendNotesTransactionScriptError,
184};
185pub use miden_tx::auth::TransactionAuthenticator;
186pub use miden_tx::{
187 DataStoreError,
188 LocalTransactionProver,
189 ProvingOptions,
190 TransactionExecutorError,
191 TransactionProverError,
192};
193pub use result::TransactionResult;
194
195impl<AUTH> Client<AUTH>
197where
198 AUTH: TransactionAuthenticator + Sync + 'static,
199{
200 pub async fn get_transactions(
205 &self,
206 filter: TransactionFilter,
207 ) -> Result<Vec<TransactionRecord>, ClientError> {
208 self.store.get_transactions(filter).await.map_err(Into::into)
209 }
210
211 pub fn new_transaction_batch(&mut self) -> BatchBuilder<'_, AUTH> {
219 let inner_data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
220 BatchBuilder {
221 client: self,
222 data_store: InMemoryBatchDataStore::new(inner_data_store),
223 pushed_txs: Vec::new(),
224 consumed_input_notes: BTreeSet::new(),
225 }
226 }
227
228 pub async fn submit_new_transaction(
237 &mut self,
238 account_id: AccountId,
239 transaction_request: TransactionRequest,
240 ) -> Result<TransactionId, ClientError> {
241 let prover = self.tx_prover.clone();
242 self.submit_new_transaction_with_prover(account_id, transaction_request, prover)
243 .await
244 }
245
246 pub async fn submit_new_transaction_with_prover(
253 &mut self,
254 account_id: AccountId,
255 transaction_request: TransactionRequest,
256 tx_prover: Arc<dyn TransactionProver>,
257 ) -> Result<TransactionId, ClientError> {
258 if !transaction_request.expected_ntx_scripts().is_empty() {
261 Box::pin(self.ensure_ntx_scripts_registered(
262 account_id,
263 transaction_request.expected_ntx_scripts(),
264 tx_prover.clone(),
265 ))
266 .await?;
267 }
268
269 let tx_result = self.execute_transaction(account_id, transaction_request).await?;
270 let tx_id = tx_result.executed_transaction().id();
271
272 let proven_transaction = self.prove_transaction_with(&tx_result, tx_prover).await?;
273 let submission_height =
274 self.submit_proven_transaction(proven_transaction, &tx_result).await?;
275
276 let tx_update =
285 Box::new(self.get_transaction_store_update(&tx_result, submission_height).await?);
286
287 if let Err(apply_err) = self.apply_transaction_update((*tx_update).clone()).await {
288 info!(
289 "apply_transaction_update failed for submitted tx {tx_id}; returning \
290 ApplyTransactionAfterSubmitFailed with the pending update attached: {apply_err}"
291 );
292 return Err(ClientError::ApplyTransactionAfterSubmitFailed {
293 pending_update: tx_update,
294 source: Box::new(apply_err),
295 });
296 }
297
298 for observer in &self.transaction_observers {
302 crate::errors::log_observer_failure(
303 observer.name(),
304 "TransactionObserver::apply",
305 observer.apply(&tx_result).await,
306 );
307 }
308
309 Ok(tx_id)
310 }
311
312 pub async fn execute_transaction(
322 &self,
323 account_id: AccountId,
324 transaction_request: TransactionRequest,
325 ) -> Result<TransactionResult, ClientError> {
326 self.execute_transaction_with_mode(
327 account_id,
328 transaction_request,
329 TransactionExecutionMode::Standard,
330 None,
331 )
332 .await
333 }
334
335 pub async fn execute_transaction_at(
363 &mut self,
364 account_id: AccountId,
365 transaction_request: TransactionRequest,
366 anchor: ChainAnchor,
367 ) -> Result<TransactionResult, ClientError> {
368 let result = self
369 .execute_transaction_with_mode(
370 account_id,
371 transaction_request,
372 TransactionExecutionMode::Standard,
373 Some(Box::new(anchor)),
374 )
375 .await?;
376
377 let expiration = result.executed_transaction().expiration_block_num();
382 let sync_height = self.store.get_sync_height().await?;
383 if expiration <= sync_height {
384 return Err(
385 ChainAnchorError::AnchoredTransactionExpired { expiration, sync_height }.into()
386 );
387 }
388
389 Ok(result)
390 }
391
392 async fn chain_anchor_at_tip(
397 &self,
398 tracked_blocks: BTreeSet<BlockNumber>,
399 ) -> Result<ChainAnchor, ClientError> {
400 let sync_height = self.store.get_sync_height().await?;
401
402 let (header, _had_notes) = self
403 .store
404 .get_block_header_by_num(sync_height)
405 .await?
406 .ok_or(StoreError::BlockHeaderNotFound(sync_height))?;
407
408 let mut tracked_blocks = tracked_blocks;
409 tracked_blocks.remove(&sync_height);
411
412 let block_headers: Vec<BlockHeader> = self
413 .store
414 .get_block_headers(&tracked_blocks)
415 .await?
416 .into_iter()
417 .map(|(header, _has_notes)| header)
418 .collect();
419
420 let fetched_nums: BTreeSet<BlockNumber> =
423 block_headers.iter().map(BlockHeader::block_num).collect();
424 if let Some(&missing) = tracked_blocks.difference(&fetched_nums).next() {
425 return Err(StoreError::BlockHeaderNotFound(missing).into());
426 }
427
428 let peaks = self.store.get_current_blockchain_peaks().await?;
429 let partial_mmr = build_partial_mmr_with_paths(&self.store, peaks, &block_headers).await?;
430
431 let chain = PartialBlockchain::new(partial_mmr, block_headers)?;
432
433 Ok(ChainAnchor::new(header, chain)?)
434 }
435
436 pub async fn chain_anchor_for_request(
454 &self,
455 transaction_request: &TransactionRequest,
456 ) -> Result<ChainAnchor, ClientError> {
457 let input_note_ids: Vec<NoteId> = transaction_request.input_note_ids().collect();
458
459 let tracked_blocks: BTreeSet<BlockNumber> = if input_note_ids.is_empty() {
460 BTreeSet::new()
461 } else {
462 self.store
463 .get_input_notes(NoteFilter::List(input_note_ids))
464 .await?
465 .iter()
466 .filter(|record| record.is_authenticated())
467 .filter_map(|record| record.inclusion_proof())
468 .map(|proof| proof.location().block_num())
469 .collect()
470 };
471
472 self.chain_anchor_at_tip(tracked_blocks).await
473 }
474
475 #[cfg(feature = "dap")]
489 pub async fn execute_transaction_with_dap(
490 &self,
491 account_id: AccountId,
492 transaction_request: TransactionRequest,
493 ) -> Result<TransactionResult, ClientError> {
494 self.execute_transaction_with_mode(
495 account_id,
496 transaction_request,
497 TransactionExecutionMode::Dap,
498 None,
499 )
500 .await
501 }
502
503 async fn execute_transaction_with_mode(
507 &self,
508 account_id: AccountId,
509 transaction_request: TransactionRequest,
510 execution_mode: TransactionExecutionMode,
511 anchor: Option<Box<ChainAnchor>>,
512 ) -> Result<TransactionResult, ClientError> {
513 let account: Account = self.get_native_account_record(account_id).await?.try_into()?;
514
515 validate_account_request(&transaction_request, &account)?;
516 let prep = self
517 .prepare_transaction(account.code_interface(), transaction_request, anchor.as_deref())
518 .await?;
519
520 let mut data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
521 if let Some(anchor) = anchor {
522 data_store = data_store.with_chain_anchor(*anchor);
523 }
524 data_store.register_note_scripts(prep.output_note_scripts());
525 for fpi_account in &prep.foreign_account_inputs {
526 data_store.mast_store().load_account_code(fpi_account.code());
527 }
528 data_store.register_foreign_account_inputs(prep.foreign_account_inputs);
529
530 data_store.mast_store().load_account_code(account.code());
531
532 let mut notes = prep.notes;
533 if prep.ignore_invalid_notes {
534 notes = self
535 .get_valid_input_notes(
536 &data_store,
537 account.id(),
538 prep.block_num,
539 notes,
540 prep.tx_args.clone(),
541 )
542 .await?;
543 }
544
545 let executed_transaction = match execution_mode {
546 TransactionExecutionMode::Standard => {
547 self.build_executor(&data_store)?
548 .execute_transaction(account_id, prep.block_num, notes, prep.tx_args)
549 .await?
550 },
551 #[cfg(feature = "dap")]
552 TransactionExecutionMode::Dap => {
553 self.build_dap_executor(&data_store)?
554 .execute_transaction(account_id, prep.block_num, notes, prep.tx_args)
555 .await?
556 },
557 };
558
559 validate_executed_transaction(&executed_transaction, &prep.output_recipients)?;
560 TransactionResult::new(executed_transaction, prep.future_notes)
561 }
562
563 pub(crate) async fn prepare_transaction(
579 &self,
580 account_code_interface: AccountCodeInterface,
581 transaction_request: TransactionRequest,
582 anchor: Option<&ChainAnchor>,
583 ) -> Result<PreparedTransaction, ClientError> {
584 if anchor.is_none() {
585 self.validate_recency().await?;
586 }
587
588 let mut stored_note_records = self
590 .store
591 .get_input_notes(NoteFilter::List(transaction_request.input_note_ids().collect()))
592 .await?;
593
594 for note in &stored_note_records {
596 if note.is_consumed() {
597 let id = note.id().expect(
598 "stored note records reaching this check carry metadata so id() is Some",
599 );
600 return Err(ClientError::TransactionRequestError(
601 TransactionRequestError::InputNoteAlreadyConsumed(id),
602 ));
603 }
604 }
605
606 stored_note_records.retain(InputNoteRecord::is_authenticated);
608
609 let notes = transaction_request.build_input_notes(stored_note_records)?;
610
611 if let Some(anchor) = anchor {
615 for note in notes.iter() {
616 if let Some(location) = note.location() {
617 let block_num = location.block_num();
618 if block_num < anchor.block_num()
619 && !anchor.partial_blockchain().contains_block(block_num)
620 {
621 return Err(ChainAnchorError::BlockNotTracked { block_num }.into());
622 }
623 }
624 }
625 }
626
627 let output_recipients =
628 transaction_request.expected_output_recipients().cloned().collect::<Vec<_>>();
629
630 let future_notes: Vec<(NoteDetails, NoteTag)> =
631 transaction_request.expected_future_notes().cloned().collect();
632
633 let tx_script = transaction_request.build_transaction_script(&account_code_interface)?;
634
635 let foreign_accounts = transaction_request.foreign_accounts().clone();
636
637 let block_num = match anchor {
640 Some(anchor) => anchor.block_num(),
641 None => self.store.get_sync_height().await?,
642 };
643
644 let foreign_account_inputs =
645 self.retrieve_foreign_account_inputs(foreign_accounts, block_num).await?;
646
647 let ignore_invalid_notes = transaction_request.ignore_invalid_input_notes();
648
649 let tx_args = transaction_request.into_transaction_args(tx_script);
650
651 Ok(PreparedTransaction {
652 notes,
653 output_recipients,
654 future_notes,
655 tx_args,
656 foreign_account_inputs,
657 block_num,
658 ignore_invalid_notes,
659 })
660 }
661
662 pub async fn prove_transaction(
664 &self,
665 tx_result: &TransactionResult,
666 ) -> Result<ProvenTransaction, ClientError> {
667 self.prove_transaction_with(tx_result, self.tx_prover.clone()).await
668 }
669
670 pub async fn prove_transaction_with(
678 &self,
679 tx_result: &TransactionResult,
680 tx_prover: Arc<dyn TransactionProver>,
681 ) -> Result<ProvenTransaction, ClientError> {
682 info!("Proving transaction...");
683
684 let executed_transaction = tx_result.executed_transaction();
685 let proven_transaction = tx_prover.prove(executed_transaction.clone().into()).await?;
686
687 if proven_transaction.id() != executed_transaction.id() {
696 return Err(ClientError::MismatchedProvenTransaction {
697 requested: executed_transaction.id(),
698 returned: proven_transaction.id(),
699 });
700 }
701
702 info!("Transaction proven.");
703
704 Ok(proven_transaction)
705 }
706
707 pub async fn submit_proven_transaction(
710 &mut self,
711 proven_transaction: ProvenTransaction,
712 transaction_inputs: impl Into<TransactionInputs>,
713 ) -> Result<BlockNumber, ClientError> {
714 info!("Submitting transaction to the network...");
715 let tx_id = proven_transaction.id();
716 let key = self.transaction_encryption_key().await?;
717 let sealed_inputs =
718 seal_transaction_inputs(&mut self.rng, &key, tx_id, &transaction_inputs.into())?;
719 let result =
720 self.rpc_api.submit_proven_transaction(proven_transaction, sealed_inputs).await;
721 if let Err(err) = &result {
722 self.forget_stale_transaction_encryption_key(err).await;
723 }
724 let block_num = result?;
725 info!("Transaction submitted.");
726
727 Ok(block_num)
728 }
729
730 pub(crate) async fn transaction_encryption_key(
738 &self,
739 ) -> Result<TransactionEncryptionKey, ClientError> {
740 if let Some(key) = self.store.get_transaction_encryption_key().await? {
741 return Ok(key);
742 }
743
744 let attested = self.rpc_api.get_transaction_encryption_key().await?;
745
746 let genesis_commitment =
750 self.trusted_block_header(BlockNumber::GENESIS).await?.commitment();
751 let chain_tip = self.store.get_sync_height().await?;
752 let validator_keys = self.trusted_block_header(chain_tip).await?.validator_keys().clone();
753
754 let key = attested.verify(genesis_commitment, &validator_keys)?;
755 self.store.set_transaction_encryption_key(&key).await?;
756
757 Ok(key)
758 }
759
760 #[cfg(feature = "testing")]
763 pub async fn seed_transaction_encryption_key(
764 &self,
765 key: TransactionEncryptionKey,
766 ) -> Result<(), ClientError> {
767 Ok(self.store.set_transaction_encryption_key(&key).await?)
768 }
769
770 pub(crate) async fn forget_stale_transaction_encryption_key(&self, err: &RpcError) {
776 if err.is_stale_transaction_encryption_key()
777 && let Err(err) = self.store.remove_transaction_encryption_key().await
778 {
779 tracing::warn!("failed to evict the stale transaction encryption key: {err}");
780 }
781 }
782
783 async fn trusted_block_header(
790 &self,
791 block_num: BlockNumber,
792 ) -> Result<BlockHeader, ClientError> {
793 self.store.get_block_header_by_num(block_num).await?.map(|(header, _)| header).ok_or_else(
794 || {
795 ClientError::ChainValidationError(alloc::format!(
796 "block header {block_num} is not tracked locally; sync the client before it can verify data against the chain"
797 ))
798 },
799 )
800 }
801
802 pub async fn get_transaction_store_update(
805 &self,
806 tx_result: &TransactionResult,
807 submission_height: BlockNumber,
808 ) -> Result<TransactionStoreUpdate, TransactionStoreUpdateError> {
809 let note_updates = self.get_note_updates(submission_height, tx_result).await?;
810
811 let new_tags: Vec<NoteTagRecord> = note_updates
814 .updated_input_notes()
815 .filter_map(|note| {
816 let note = note.inner();
817
818 if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) =
819 note.state()
820 {
821 Some(NoteTagRecord::with_note_source(*tag, note.details_commitment()))
822 } else {
823 None
824 }
825 })
826 .collect();
827
828 Ok(TransactionStoreUpdate::new(
829 tx_result.executed_transaction().clone(),
830 submission_height,
831 note_updates,
832 tx_result.future_notes().to_vec(),
833 new_tags,
834 ))
835 }
836
837 pub async fn apply_transaction(
840 &self,
841 tx_result: &TransactionResult,
842 submission_height: BlockNumber,
843 ) -> Result<(), ClientError> {
844 let tx_update = self.get_transaction_store_update(tx_result, submission_height).await?;
845
846 self.apply_transaction_update(tx_update).await?;
847
848 for observer in &self.transaction_observers {
850 if let Err(err) = observer.apply(tx_result).await {
851 tracing::warn!(
852 observer = observer.name(),
853 error = ?err,
854 "TransactionObserver::apply failed; continuing with remaining observers",
855 );
856 }
857 }
858
859 Ok(())
860 }
861
862 pub async fn apply_transaction_update(
863 &self,
864 tx_update: TransactionStoreUpdate,
865 ) -> Result<(), ClientError> {
866 info!("Applying transaction to the local store...");
869
870 let executed_transaction = tx_update.executed_transaction();
871 let account_id = executed_transaction.account_id();
872
873 if self.account_reader(account_id).status().await?.is_locked() {
874 return Err(ClientError::AccountLocked(account_id));
875 }
876
877 self.store.apply_transaction(tx_update).await?;
878 info!("Transaction stored.");
879 Ok(())
880 }
881
882 pub async fn execute_program(
887 &self,
888 account_id: AccountId,
889 tx_script: TransactionScript,
890 advice_inputs: AdviceInputs,
891 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
892 ) -> Result<[Felt; MIN_STACK_DEPTH], ClientError> {
893 let (data_store, block_ref) =
894 self.prepare_program_execution(account_id, foreign_accounts).await?;
895
896 Ok(self
897 .build_executor(&data_store)?
898 .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
899 .await?)
900 }
901
902 #[cfg(feature = "dap")]
905 pub async fn execute_program_with_dap(
906 &self,
907 account_id: AccountId,
908 tx_script: TransactionScript,
909 advice_inputs: AdviceInputs,
910 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
911 ) -> Result<[Felt; MIN_STACK_DEPTH], ClientError> {
912 let (data_store, block_ref) =
913 self.prepare_program_execution(account_id, foreign_accounts).await?;
914
915 Ok(self
916 .build_dap_executor(&data_store)?
917 .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
918 .await?)
919 }
920
921 pub async fn validate_request(
931 &self,
932 account_id: AccountId,
933 transaction_request: &TransactionRequest,
934 ) -> Result<(), ClientError> {
935 self.validate_recency().await?;
936 validate_output_note_senders(transaction_request, account_id)?;
937 let account = self.try_get_account(account_id).await?;
938 validate_account_request(transaction_request, &account)
939 }
940
941 async fn validate_recency(&self) -> Result<(), ClientError> {
942 if let Some(max_block_number_delta) = self.max_block_number_delta {
943 let current_chain_tip =
944 self.rpc_api.get_block_header_by_number(None, false).await?.0.block_num();
945
946 if current_chain_tip > self.store.get_sync_height().await? + max_block_number_delta {
947 return Err(ClientError::RecencyConditionError(
948 "The client is too far behind the chain tip to execute the transaction",
949 ));
950 }
951 }
952 Ok(())
953 }
954
955 pub async fn ensure_ntx_scripts_registered(
968 &mut self,
969 account_id: AccountId,
970 scripts: &[NoteScript],
971 tx_prover: Arc<dyn TransactionProver>,
972 ) -> Result<(), ClientError> {
973 let mut missing_scripts = Vec::new();
974
975 for script in scripts {
976 if StandardNote::from_script(script).is_some() {
978 continue;
979 }
980
981 let script_root = script.root();
982
983 match self.rpc_api.get_note_script_by_root(script_root.into()).await {
985 Ok(Some(_)) => {},
986 Ok(None) => missing_scripts.push(script.clone()),
987 Err(source) => {
988 return Err(ClientError::NtxScriptRegistrationFailed {
989 script_root: script_root.into(),
990 source,
991 });
992 },
993 }
994 }
995
996 if missing_scripts.is_empty() {
997 return Ok(());
998 }
999
1000 let registration_request = TransactionRequestBuilder::new().build_register_note_scripts(
1001 account_id,
1002 missing_scripts,
1003 self.rng(),
1004 )?;
1005
1006 let tx_result = self.execute_transaction(account_id, registration_request).await?;
1007 let proven = self.prove_transaction_with(&tx_result, tx_prover).await?;
1008 let submission_height = self.submit_proven_transaction(proven, &tx_result).await?;
1009 self.apply_transaction(&tx_result, submission_height).await?;
1010
1011 Ok(())
1012 }
1013
1014 pub(crate) async fn get_valid_input_notes<STORE: DataStore + Sync>(
1019 &self,
1020 data_store: &STORE,
1021 account_id: AccountId,
1022 block_ref: BlockNumber,
1023 mut input_notes: InputNotes<InputNote>,
1024 tx_args: TransactionArgs,
1025 ) -> Result<InputNotes<InputNote>, ClientError> {
1026 loop {
1027 if input_notes.is_empty() {
1030 break;
1031 }
1032
1033 let execution = NoteConsumptionChecker::new(&self.build_executor(data_store)?)
1034 .check_notes_consumability(
1035 account_id,
1036 block_ref,
1037 input_notes.iter().map(|n| n.clone().into_note()).collect(),
1038 tx_args.clone(),
1039 )
1040 .await?;
1041
1042 if execution.failed().is_empty() {
1043 break;
1044 }
1045
1046 let failed_note_ids: BTreeSet<NoteId> =
1047 execution.failed().iter().map(|n| n.note().id()).collect();
1048 let filtered_input_notes = InputNotes::new(
1049 input_notes
1050 .into_iter()
1051 .filter(|note| !failed_note_ids.contains(¬e.id()))
1052 .collect(),
1053 )
1054 .expect("Created from a valid input notes list");
1055
1056 input_notes = filtered_input_notes;
1057 }
1058
1059 Ok(input_notes)
1060 }
1061
1062 async fn retrieve_foreign_account_inputs(
1071 &self,
1072 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
1073 block_num: BlockNumber,
1074 ) -> Result<Vec<AccountInputs>, ClientError> {
1075 if foreign_accounts.is_empty() {
1076 return Ok(Vec::new());
1077 }
1078
1079 let mut return_foreign_account_inputs = Vec::with_capacity(foreign_accounts.len());
1080
1081 for foreign_account in foreign_accounts.into_values() {
1082 let foreign_account_inputs = match foreign_account {
1083 ForeignAccount::Public(account_id, storage_requirements) => {
1084 fetch_public_account_inputs(
1085 &self.store,
1086 &self.rpc_api,
1087 account_id,
1088 storage_requirements,
1089 AccountStateAt::Block(block_num),
1090 )
1091 .await?
1092 },
1093 ForeignAccount::Private(partial_account) => {
1094 let account_id = partial_account.id();
1095 let (_, account_proof) = self
1096 .rpc_api
1097 .get_account(
1098 account_id,
1099 GetAccountRequest::new().at(AccountStateAt::Block(block_num)),
1100 )
1101 .await?;
1102 let (witness, _) = account_proof.into_parts();
1103 AccountInputs::new(partial_account, witness)
1104 },
1105 };
1106
1107 return_foreign_account_inputs.push(foreign_account_inputs);
1108 }
1109
1110 Ok(return_foreign_account_inputs)
1111 }
1112
1113 async fn prepare_program_execution(
1117 &self,
1118 account_id: AccountId,
1119 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
1120 ) -> Result<(ClientDataStore, BlockNumber), ClientError> {
1121 let block_ref = self.get_sync_height().await?;
1122
1123 let foreign_account_inputs =
1124 self.retrieve_foreign_account_inputs(foreign_accounts, block_ref).await?;
1125
1126 let account_code = self
1127 .store
1128 .get_account_code(account_id)
1129 .await?
1130 .ok_or(ClientError::AccountDataNotFound(account_id))?;
1131
1132 let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
1133
1134 data_store.mast_store().load_account_code(&account_code);
1136
1137 for fpi_account in &foreign_account_inputs {
1138 data_store.mast_store().load_account_code(fpi_account.code());
1139 }
1140
1141 data_store.register_foreign_account_inputs(foreign_account_inputs);
1142
1143 Ok((data_store, block_ref))
1144 }
1145
1146 pub(crate) fn build_executor<'store, 'auth, STORE: DataStore + Sync>(
1149 &'auth self,
1150 data_store: &'store STORE,
1151 ) -> Result<TransactionExecutor<'store, 'auth, STORE, AUTH>, TransactionExecutorError> {
1152 let mut executor = TransactionExecutor::new(data_store)
1153 .with_options(self.exec_options)?
1154 .with_source_manager(self.source_manager.clone());
1155 if let Some(authenticator) = self.authenticator.as_deref() {
1156 executor = executor.with_authenticator(authenticator);
1157 }
1158 Ok(executor)
1159 }
1160
1161 async fn get_native_account_record(
1164 &self,
1165 account_id: AccountId,
1166 ) -> Result<AccountRecord, ClientError> {
1167 let account_record = self
1168 .store
1169 .get_account(account_id)
1170 .await?
1171 .ok_or(ClientError::AccountDataNotFound(account_id))?;
1172 if account_record.is_watched() {
1173 return Err(ClientError::AccountIsWatched(account_id));
1174 }
1175 Ok(account_record)
1176 }
1177
1178 #[cfg(feature = "dap")]
1180 pub(crate) fn build_dap_executor<'store, 'auth, STORE: DataStore + Sync>(
1181 &'auth self,
1182 data_store: &'store STORE,
1183 ) -> Result<
1184 TransactionExecutor<'store, 'auth, STORE, AUTH, dap_executor::DapProgramExecutor>,
1185 TransactionExecutorError,
1186 > {
1187 Ok(self
1188 .build_executor(data_store)?
1189 .with_program_executor::<dap_executor::DapProgramExecutor>())
1190 }
1191
1192 async fn get_note_updates(
1195 &self,
1196 submission_height: BlockNumber,
1197 tx_result: &TransactionResult,
1198 ) -> Result<NoteUpdateTracker, TransactionStoreUpdateError> {
1199 let executed_tx = tx_result.executed_transaction();
1200 let current_timestamp = self.store.get_current_timestamp();
1201 let current_block_num = self.store.get_sync_height().await?;
1202
1203 let new_output_notes = executed_tx
1205 .output_notes()
1206 .iter()
1207 .cloned()
1208 .filter_map(|output_note| {
1209 OutputNoteRecord::try_from_output_note(output_note, submission_height).ok()
1210 })
1211 .collect::<Vec<_>>();
1212
1213 let mut new_input_notes = vec![];
1215 let output_notes: Vec<Note> =
1216 notes_from_output(executed_tx.output_notes()).cloned().collect();
1217 let note_screener = self.note_screener().clone();
1218 let output_note_relevances = note_screener.get_batch_consumability(&output_notes).await?;
1219
1220 for note in output_notes {
1221 if output_note_relevances.contains_key(¬e.id()) {
1222 let metadata = *note.metadata();
1223 let tag = metadata.tag();
1224 let attachments = note.attachments().clone();
1225
1226 new_input_notes.push(InputNoteRecord::new(
1227 note.into(),
1228 attachments,
1229 current_timestamp,
1230 ExpectedNoteState {
1231 metadata: Some(metadata),
1232 after_block_num: submission_height,
1233 tag: Some(tag),
1234 }
1235 .into(),
1236 ));
1237 }
1238 }
1239
1240 new_input_notes.extend(tx_result.future_notes().iter().map(|(note_details, tag)| {
1242 InputNoteRecord::new(
1243 note_details.clone(),
1244 NoteAttachments::empty(),
1245 None,
1246 ExpectedNoteState {
1247 metadata: None,
1248 after_block_num: current_block_num,
1249 tag: Some(*tag),
1250 }
1251 .into(),
1252 )
1253 }));
1254
1255 let consumed_note_ids =
1260 executed_tx.tx_inputs().input_notes().iter().map(InputNote::id).collect();
1261
1262 let consumed_notes =
1263 self.store.get_input_notes(NoteFilter::List(consumed_note_ids)).await?;
1264
1265 let tracked_note_ids =
1266 consumed_notes.iter().filter_map(InputNoteRecord::id).collect::<BTreeSet<_>>();
1267
1268 for input_note in executed_tx.tx_inputs().input_notes() {
1269 if !tracked_note_ids.contains(&input_note.id()) {
1270 let mut input_note_record = InputNoteRecord::from(input_note.clone());
1271 input_note_record.consumed_locally(
1272 executed_tx.account_id(),
1273 executed_tx.id(),
1274 current_timestamp,
1275 )?;
1276 new_input_notes.push(input_note_record);
1277 }
1278 }
1279
1280 let mut updated_input_notes = vec![];
1281
1282 for mut input_note_record in consumed_notes {
1283 if input_note_record.consumed_locally(
1284 executed_tx.account_id(),
1285 executed_tx.id(),
1286 current_timestamp,
1287 )? {
1288 updated_input_notes.push(input_note_record);
1289 }
1290 }
1291
1292 Ok(NoteUpdateTracker::for_transaction_updates(
1293 new_input_notes,
1294 updated_input_notes,
1295 new_output_notes,
1296 ))
1297 }
1298}
1299
1300#[derive(Debug, thiserror::Error)]
1306pub enum TransactionStoreUpdateError {
1307 #[error("store error")]
1308 Store(#[from] StoreError),
1309 #[error("note screener error")]
1310 NoteScreener(#[from] NoteScreenerError),
1311 #[error("note record error")]
1312 NoteRecord(#[from] NoteRecordError),
1313}
1314
1315#[derive(Clone, Copy, Debug)]
1319enum TransactionExecutionMode {
1320 Standard,
1321 #[cfg(feature = "dap")]
1322 Dap,
1323}
1324
1325pub(crate) struct PreparedTransaction {
1327 pub(crate) notes: InputNotes<InputNote>,
1328 pub(crate) output_recipients: Vec<NoteRecipient>,
1329 pub(crate) future_notes: Vec<(NoteDetails, NoteTag)>,
1330 pub(crate) tx_args: TransactionArgs,
1331 pub(crate) foreign_account_inputs: Vec<AccountInputs>,
1332 pub(crate) block_num: BlockNumber,
1333 pub(crate) ignore_invalid_notes: bool,
1334}
1335
1336impl PreparedTransaction {
1337 pub(crate) fn output_note_scripts(&self) -> impl Iterator<Item = NoteScript> + '_ {
1340 self.output_recipients.iter().map(|recipient| recipient.script().clone())
1341 }
1342}
1343
1344fn get_outgoing_assets(
1349 transaction_request: &TransactionRequest,
1350) -> (BTreeMap<AccountId, u64>, Vec<NonFungibleAsset>) {
1351 let mut own_notes_assets = match transaction_request.script_template() {
1353 Some(TransactionScriptTemplate::SendNotes(notes)) => notes
1354 .iter()
1355 .map(|note| (note.id(), note.assets().clone()))
1356 .collect::<BTreeMap<_, _>>(),
1357 _ => BTreeMap::default(),
1358 };
1359 let mut output_notes_assets = transaction_request
1361 .expected_output_own_notes()
1362 .into_iter()
1363 .map(|note| (note.id(), note.assets().clone()))
1364 .collect::<BTreeMap<_, _>>();
1365
1366 output_notes_assets.append(&mut own_notes_assets);
1368
1369 let outgoing_assets = output_notes_assets.values().flat_map(|note_assets| note_assets.iter());
1371
1372 request::collect_assets(outgoing_assets)
1373}
1374
1375pub(super) fn validate_account_request(
1379 transaction_request: &TransactionRequest,
1380 account: &Account,
1381) -> Result<(), ClientError> {
1382 validate_fee_conversion_info_support(transaction_request, account)?;
1383
1384 if account.code_interface().contains([FungibleFaucet::mint_and_send_root()]) {
1385 Ok(())
1387 } else {
1388 validate_basic_account_request(transaction_request, account)
1389 }
1390}
1391
1392fn validate_fee_conversion_info_support(
1399 transaction_request: &TransactionRequest,
1400 account: &Account,
1401) -> Result<(), ClientError> {
1402 if !transaction_request.declares_fee_conversion_info() {
1403 return Ok(());
1404 }
1405
1406 let interface = AccountInterface::from_account(account);
1407 let auth_component = interface.auth_component();
1408 if matches!(
1409 auth_component,
1410 AccountComponentInterface::AuthSingleSig | AccountComponentInterface::AuthMultisig
1411 ) {
1412 return Ok(());
1413 }
1414
1415 Err(ClientError::TransactionRequestError(
1416 TransactionRequestError::FeeConversionInfoUnsupported(auth_component.name()),
1417 ))
1418}
1419
1420fn validate_output_note_senders(
1428 transaction_request: &TransactionRequest,
1429 account_id: AccountId,
1430) -> Result<(), ClientError> {
1431 for note in transaction_request.expected_output_own_notes() {
1432 let sender = note.metadata().sender();
1433 if sender != account_id {
1434 return Err(ClientError::TransactionRequestError(
1435 TransactionRequestError::OutputNoteSenderMismatch {
1436 expected: account_id,
1437 actual: sender,
1438 },
1439 ));
1440 }
1441 }
1442
1443 Ok(())
1444}
1445
1446fn validate_basic_account_request(
1449 transaction_request: &TransactionRequest,
1450 account: &Account,
1451) -> Result<(), ClientError> {
1452 let (fungible_balance_map, non_fungible_set) = get_outgoing_assets(transaction_request);
1454
1455 let (incoming_fungible_balance_map, incoming_non_fungible_balance_set) =
1457 transaction_request.incoming_assets();
1458
1459 let mut available_fungible: BTreeMap<AccountId, u64> = BTreeMap::new();
1462 for asset in account.vault().assets() {
1463 if let Asset::Fungible(fungible) = asset {
1464 let balance = available_fungible.entry(fungible.faucet_id()).or_default();
1465 *balance = balance.saturating_add(fungible.amount().as_u64());
1466 }
1467 }
1468
1469 for (faucet_id, amount) in fungible_balance_map {
1472 let account_asset_amount = available_fungible.get(&faucet_id).copied().unwrap_or(0);
1473 let incoming_balance = incoming_fungible_balance_map.get(&faucet_id).unwrap_or(&0);
1474 if account_asset_amount + incoming_balance < amount {
1475 return Err(ClientError::AssetError(AssetError::FungibleAssetAmountNotSufficient {
1476 minuend: account_asset_amount,
1477 subtrahend: amount,
1478 }));
1479 }
1480 }
1481
1482 for non_fungible in &non_fungible_set {
1485 match account.vault().has_non_fungible_asset(*non_fungible) {
1486 Ok(true) => (),
1487 Ok(false) => {
1488 if !incoming_non_fungible_balance_set.contains(non_fungible) {
1490 return Err(ClientError::TransactionRequestError(
1491 TransactionRequestError::MissingNonFungibleAsset(non_fungible.faucet_id()),
1492 ));
1493 }
1494 },
1495 _ => {
1496 return Err(ClientError::TransactionRequestError(
1497 TransactionRequestError::MissingNonFungibleAsset(non_fungible.faucet_id()),
1498 ));
1499 },
1500 }
1501 }
1502
1503 Ok(())
1504}
1505
1506pub(crate) async fn fetch_public_account_inputs(
1516 store: &Arc<dyn Store>,
1517 rpc_api: &Arc<dyn NodeRpcClient>,
1518 account_id: AccountId,
1519 storage_requirements: AccountStorageRequirements,
1520 account_state_at: AccountStateAt,
1521) -> Result<AccountInputs, ClientError> {
1522 let known_code: Option<AccountCode> =
1523 store.get_foreign_account_code(vec![account_id]).await?.into_values().next();
1524
1525 let vault = store
1528 .get_account_header(account_id)
1529 .await?
1530 .map_or(VaultFetch::Always, |(header, ..)| {
1531 VaultFetch::IfChangedFrom(header.vault_root())
1532 });
1533
1534 let (_block_num, account_proof) = rpc_api
1535 .get_account(
1536 account_id,
1537 GetAccountRequest::new()
1538 .with_storage(StorageMapFetch::Slots(storage_requirements.clone()))
1539 .at(account_state_at)
1540 .with_known_code(known_code)
1541 .with_vault(vault),
1542 )
1543 .await?;
1544
1545 let account_inputs = request::account_proof_into_inputs(account_proof)?;
1546
1547 let _ = store
1548 .upsert_foreign_account_code(account_id, account_inputs.code().clone())
1549 .await
1550 .inspect_err(|err| {
1551 tracing::warn!(
1552 %account_id,
1553 %err,
1554 "Failed to persist foreign account code to store"
1555 );
1556 });
1557
1558 Ok(account_inputs)
1559}
1560
1561pub fn notes_from_output(output_notes: &RawOutputNotes) -> impl Iterator<Item = &Note> {
1566 output_notes.iter().filter_map(|n| match n {
1567 RawOutputNote::Full(n) => Some(n),
1568 RawOutputNote::Partial(_) => None,
1569 })
1570}
1571
1572pub(crate) fn validate_executed_transaction(
1575 executed_transaction: &ExecutedTransaction,
1576 expected_output_recipients: &[NoteRecipient],
1577) -> Result<(), ClientError> {
1578 let tx_output_recipient_digests = executed_transaction
1579 .output_notes()
1580 .iter()
1581 .filter_map(|n| n.recipient().map(NoteRecipient::digest))
1582 .collect::<Vec<_>>();
1583
1584 let missing_recipient_digest: Vec<Word> = expected_output_recipients
1585 .iter()
1586 .filter_map(|recipient| {
1587 (!tx_output_recipient_digests.contains(&recipient.digest()))
1588 .then_some(recipient.digest())
1589 })
1590 .collect();
1591
1592 if !missing_recipient_digest.is_empty() {
1593 return Err(ClientError::MissingOutputRecipients(missing_recipient_digest));
1594 }
1595
1596 Ok(())
1597}
1598
1599#[cfg(test)]
1603mod tests {
1604 use alloc::vec;
1605
1606 use miden_protocol::Word;
1607 use miden_protocol::account::auth::AuthSecretKey;
1608 use miden_protocol::account::{AccountBuilder, AccountComponent, AccountId, AccountType};
1609 use miden_protocol::asset::FungibleAsset;
1610 use miden_protocol::crypto::rand::RandomCoin;
1611 use miden_protocol::note::{Note, NoteType};
1612 use miden_protocol::testing::account_id::{
1613 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
1614 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
1615 ACCOUNT_ID_SENDER,
1616 };
1617 use miden_standards::account::AccountBuilderSchemaCommitmentExt;
1618 use miden_standards::account::auth::{Approver, AuthSingleSig, FeeConversionInfo, NoAuth};
1619 use miden_standards::account::wallets::BasicWallet;
1620 use miden_standards::note::P2idNote;
1621
1622 use super::{
1623 Account,
1624 AccountComponentInterface,
1625 TransactionRequest,
1626 TransactionRequestBuilder,
1627 validate_fee_conversion_info_support,
1628 validate_output_note_senders,
1629 };
1630 use crate::ClientError;
1631 use crate::auth::AuthSchemeId;
1632 use crate::transaction::TransactionRequestError;
1633
1634 fn own_note_with_sender(sender: AccountId) -> Note {
1635 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
1636 let target_id =
1637 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1638 let mut rng = RandomCoin::new(Word::default());
1639
1640 P2idNote::builder()
1641 .sender(sender)
1642 .target(target_id)
1643 .asset(FungibleAsset::new(faucet_id, 100).unwrap())
1644 .note_type(NoteType::Public)
1645 .generate_serial_number(&mut rng)
1646 .build()
1647 .expect("note creation failed")
1648 .into()
1649 }
1650
1651 #[test]
1652 fn output_note_with_foreign_sender_is_rejected() {
1653 let account_id =
1654 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1655 let foreign_sender = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
1656 assert_ne!(account_id, foreign_sender);
1657
1658 let request = TransactionRequestBuilder::new()
1659 .own_output_notes(vec![own_note_with_sender(foreign_sender)])
1660 .build()
1661 .unwrap();
1662
1663 let err = validate_output_note_senders(&request, account_id).unwrap_err();
1664 match err {
1665 ClientError::TransactionRequestError(
1666 TransactionRequestError::OutputNoteSenderMismatch { expected, actual },
1667 ) => {
1668 assert_eq!(expected, account_id);
1669 assert_eq!(actual, foreign_sender);
1670 },
1671 other => panic!("expected OutputNoteSenderMismatch, got {other:?}"),
1672 }
1673 }
1674
1675 #[test]
1676 fn output_note_with_matching_sender_is_accepted() {
1677 let account_id =
1678 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1679
1680 let request = TransactionRequestBuilder::new()
1681 .own_output_notes(vec![own_note_with_sender(account_id)])
1682 .build()
1683 .unwrap();
1684
1685 validate_output_note_senders(&request, account_id).unwrap();
1686 }
1687
1688 #[test]
1689 fn request_without_own_output_notes_is_accepted() {
1690 let account_id =
1691 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1692 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
1693
1694 let request = TransactionRequestBuilder::new()
1696 .input_notes(vec![(own_note_with_sender(faucet_id), None)])
1697 .build()
1698 .unwrap();
1699
1700 validate_output_note_senders(&request, account_id).unwrap();
1701 }
1702
1703 fn account_with_auth(auth_component: impl Into<AccountComponent>) -> Account {
1705 AccountBuilder::new([7u8; 32])
1706 .account_type(AccountType::Public)
1707 .with_component(auth_component)
1708 .with_component(BasicWallet)
1709 .build_with_schema_commitment()
1710 .expect("account creation failed")
1711 }
1712
1713 fn fee_conversion_request() -> TransactionRequest {
1714 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
1715
1716 TransactionRequestBuilder::new()
1717 .fee_conversion_info(FeeConversionInfo::one_to_one(faucet_id), Word::default())
1718 .build()
1719 .unwrap()
1720 }
1721
1722 #[test]
1723 fn fee_conversion_info_is_accepted_by_a_signature_authenticated_account() {
1724 let key = AuthSecretKey::new_falcon512_poseidon2();
1725 let auth = AuthSingleSig::new(Approver::new(
1726 key.public_key().to_commitment(),
1727 AuthSchemeId::Falcon512Poseidon2,
1728 ));
1729
1730 validate_fee_conversion_info_support(&fee_conversion_request(), &account_with_auth(auth))
1731 .unwrap();
1732 }
1733
1734 #[test]
1735 fn fee_conversion_info_is_rejected_by_an_account_that_cannot_read_it() {
1736 let account = account_with_auth(NoAuth);
1737
1738 let err = validate_fee_conversion_info_support(&fee_conversion_request(), &account)
1739 .expect_err("NoAuth does not read the auth args");
1740 match err {
1741 ClientError::TransactionRequestError(
1742 TransactionRequestError::FeeConversionInfoUnsupported(auth_component),
1743 ) => assert_eq!(auth_component, AccountComponentInterface::AuthNoAuth.name()),
1744 other => panic!("expected FeeConversionInfoUnsupported, got {other:?}"),
1745 }
1746 }
1747
1748 #[test]
1749 fn a_request_without_fee_conversion_info_skips_the_auth_component_check() {
1750 validate_fee_conversion_info_support(
1752 &TransactionRequestBuilder::new().build().unwrap(),
1753 &account_with_auth(NoAuth),
1754 )
1755 .unwrap();
1756 }
1757}