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