1use alloc::boxed::Box;
66use alloc::collections::{BTreeMap, BTreeSet};
67use alloc::sync::Arc;
68use alloc::vec::Vec;
69
70use miden_protocol::account::{Account, AccountCode, AccountId};
71use miden_protocol::asset::{Asset, NonFungibleAsset};
72use miden_protocol::block::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;
84use miden_protocol::vm::MIN_STACK_DEPTH;
85use miden_protocol::{Felt, Word};
86use miden_standards::account::faucets::FungibleFaucet;
87use miden_tx::{DataStore, NoteConsumptionChecker, TransactionExecutor};
88use tracing::info;
89
90use super::Client;
91use crate::ClientError;
92use crate::note::{NoteScreenerError, NoteUpdateTracker, StandardNote};
93use crate::rpc::domain::account::{
94 AccountStorageRequirements,
95 GetAccountRequest,
96 StorageMapFetch,
97 VaultFetch,
98};
99use crate::rpc::{AccountStateAt, NodeRpcClient};
100use crate::store::data_store::ClientDataStore;
101use crate::store::input_note_states::ExpectedNoteState;
102use crate::store::{
103 AccountRecord,
104 InputNoteRecord,
105 InputNoteState,
106 NoteFilter,
107 NoteRecordError,
108 OutputNoteRecord,
109 Store,
110 StoreError,
111 TransactionFilter,
112};
113use crate::sync::NoteTagRecord;
114use crate::transaction::batch::InMemoryBatchDataStore;
115
116pub mod batch;
117pub use batch::{BatchBuilder, BatchBuilderError};
118
119#[cfg(feature = "dap")]
120mod dap_executor;
121mod prover;
122pub use prover::TransactionProver;
123
124mod record;
125pub use record::{
126 DiscardCause,
127 TransactionDetails,
128 TransactionRecord,
129 TransactionStatus,
130 TransactionStatusVariant,
131};
132
133mod store_update;
134pub use store_update::TransactionStoreUpdate;
135
136mod request;
137pub use request::{
138 ForeignAccount,
139 NoteArgs,
140 PaymentNoteDescription,
141 PswapTransactionData,
142 SwapTransactionData,
143 TransactionRequest,
144 TransactionRequestBuilder,
145 TransactionRequestError,
146 TransactionScriptTemplate,
147};
148
149mod observer;
150pub use observer::TransactionObserver;
151
152mod result;
153pub use miden_protocol::transaction::{
156 ExecutedTransaction,
157 InputNote,
158 InputNotes,
159 OutputNote,
160 OutputNotes,
161 ProvenTransaction,
162 PublicOutputNote,
163 RawOutputNote,
164 RawOutputNotes,
165 TransactionArgs,
166 TransactionId,
167 TransactionInputs,
168 TransactionKernel,
169 TransactionScript,
170 TransactionScriptRoot,
171 TransactionSummary,
172};
173pub use miden_protocol::vm::{AdviceInputs, AdviceMap};
174pub use miden_standards::account::interface::{AccountComponentInterface, AccountInterface};
175pub use miden_standards::tx_script::{
176 ExpirationTransactionScript,
177 SendNotesTransactionScriptError,
178};
179pub use miden_tx::auth::TransactionAuthenticator;
180pub use miden_tx::{
181 DataStoreError,
182 LocalTransactionProver,
183 ProvingOptions,
184 TransactionExecutorError,
185 TransactionProverError,
186};
187pub use result::TransactionResult;
188
189impl<AUTH> Client<AUTH>
191where
192 AUTH: TransactionAuthenticator + Sync + 'static,
193{
194 pub async fn get_transactions(
199 &self,
200 filter: TransactionFilter,
201 ) -> Result<Vec<TransactionRecord>, ClientError> {
202 self.store.get_transactions(filter).await.map_err(Into::into)
203 }
204
205 pub fn new_transaction_batch(&self) -> BatchBuilder<'_, AUTH> {
213 let inner_data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
214 BatchBuilder {
215 client: self,
216 data_store: InMemoryBatchDataStore::new(inner_data_store),
217 pushed_txs: Vec::new(),
218 consumed_input_notes: BTreeSet::new(),
219 }
220 }
221
222 pub async fn submit_new_transaction(
231 &mut self,
232 account_id: AccountId,
233 transaction_request: TransactionRequest,
234 ) -> Result<TransactionId, ClientError> {
235 let prover = self.tx_prover.clone();
236 self.submit_new_transaction_with_prover(account_id, transaction_request, prover)
237 .await
238 }
239
240 pub async fn submit_new_transaction_with_prover(
247 &mut self,
248 account_id: AccountId,
249 transaction_request: TransactionRequest,
250 tx_prover: Arc<dyn TransactionProver>,
251 ) -> Result<TransactionId, ClientError> {
252 if !transaction_request.expected_ntx_scripts().is_empty() {
255 Box::pin(self.ensure_ntx_scripts_registered(
256 account_id,
257 transaction_request.expected_ntx_scripts(),
258 tx_prover.clone(),
259 ))
260 .await?;
261 }
262
263 let tx_result = self.execute_transaction(account_id, transaction_request).await?;
264 let tx_id = tx_result.executed_transaction().id();
265
266 let proven_transaction = self.prove_transaction_with(&tx_result, tx_prover).await?;
267 let submission_height =
268 self.submit_proven_transaction(proven_transaction, &tx_result).await?;
269
270 let tx_update =
279 Box::new(self.get_transaction_store_update(&tx_result, submission_height).await?);
280
281 if let Err(apply_err) = self.apply_transaction_update((*tx_update).clone()).await {
282 info!(
283 "apply_transaction_update failed for submitted tx {tx_id}; returning \
284 ApplyTransactionAfterSubmitFailed with the pending update attached: {apply_err}"
285 );
286 return Err(ClientError::ApplyTransactionAfterSubmitFailed {
287 pending_update: tx_update,
288 source: Box::new(apply_err),
289 });
290 }
291
292 for observer in &self.transaction_observers {
296 crate::errors::log_observer_failure(
297 observer.name(),
298 "TransactionObserver::apply",
299 observer.apply(&tx_result).await,
300 );
301 }
302
303 Ok(tx_id)
304 }
305
306 pub async fn execute_transaction(
316 &mut self,
317 account_id: AccountId,
318 transaction_request: TransactionRequest,
319 ) -> Result<TransactionResult, ClientError> {
320 let account: Account = self.get_native_account_record(account_id).await?.try_into()?;
321
322 let prep = self.prepare_transaction(&account, transaction_request).await?;
323
324 let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
325 data_store.register_note_scripts(prep.output_note_scripts());
326 for fpi_account in &prep.foreign_account_inputs {
327 data_store.mast_store().load_account_code(fpi_account.code());
328 }
329 data_store.register_foreign_account_inputs(prep.foreign_account_inputs);
330
331 data_store.mast_store().load_account_code(account.code());
332
333 let mut notes = prep.notes;
334 if prep.ignore_invalid_notes {
335 notes = self
336 .get_valid_input_notes(
337 &account,
338 notes,
339 prep.tx_args.clone(),
340 &prep.output_recipients,
341 )
342 .await?;
343 }
344
345 let executed_transaction = self
346 .build_executor(&data_store)?
347 .execute_transaction(account_id, prep.block_num, notes, prep.tx_args)
348 .await?;
349
350 validate_executed_transaction(&executed_transaction, &prep.output_recipients)?;
351 TransactionResult::new(executed_transaction, prep.future_notes)
352 }
353
354 pub(crate) async fn prepare_transaction(
366 &self,
367 account: &Account,
368 transaction_request: TransactionRequest,
369 ) -> Result<PreparedTransaction, ClientError> {
370 let account_id = account.id();
371 self.validate_recency().await?;
372 validate_account_request(&transaction_request, account)?;
373
374 let mut stored_note_records = self
376 .store
377 .get_input_notes(NoteFilter::List(transaction_request.input_note_ids().collect()))
378 .await?;
379
380 for note in &stored_note_records {
382 if note.is_consumed() {
383 let id = note.id().expect(
384 "stored note records reaching this check carry metadata so id() is Some",
385 );
386 return Err(ClientError::TransactionRequestError(
387 TransactionRequestError::InputNoteAlreadyConsumed(id),
388 ));
389 }
390 }
391
392 stored_note_records.retain(InputNoteRecord::is_authenticated);
394
395 let notes = transaction_request.build_input_notes(stored_note_records)?;
396
397 let output_recipients =
398 transaction_request.expected_output_recipients().cloned().collect::<Vec<_>>();
399
400 let future_notes: Vec<(NoteDetails, NoteTag)> =
401 transaction_request.expected_future_notes().cloned().collect();
402
403 let account = self.try_get_account(account_id).await?;
404 let tx_script = transaction_request.build_transaction_script(&account.code_interface())?;
405
406 let foreign_accounts = transaction_request.foreign_accounts().clone();
407
408 let (fpi_block_num, foreign_account_inputs) =
409 self.retrieve_foreign_account_inputs(foreign_accounts).await?;
410
411 let ignore_invalid_notes = transaction_request.ignore_invalid_input_notes();
412
413 let block_num = if let Some(block_num) = fpi_block_num {
414 block_num
415 } else {
416 self.store.get_sync_height().await?
417 };
418
419 let tx_args = transaction_request.into_transaction_args(tx_script);
420
421 Ok(PreparedTransaction {
422 notes,
423 output_recipients,
424 future_notes,
425 tx_args,
426 foreign_account_inputs,
427 block_num,
428 ignore_invalid_notes,
429 })
430 }
431
432 pub async fn prove_transaction(
434 &self,
435 tx_result: &TransactionResult,
436 ) -> Result<ProvenTransaction, ClientError> {
437 self.prove_transaction_with(tx_result, self.tx_prover.clone()).await
438 }
439
440 pub async fn prove_transaction_with(
442 &self,
443 tx_result: &TransactionResult,
444 tx_prover: Arc<dyn TransactionProver>,
445 ) -> Result<ProvenTransaction, ClientError> {
446 info!("Proving transaction...");
447
448 let proven_transaction =
449 tx_prover.prove(tx_result.executed_transaction().clone().into()).await?;
450
451 info!("Transaction proven.");
452
453 Ok(proven_transaction)
454 }
455
456 pub async fn submit_proven_transaction(
459 &mut self,
460 proven_transaction: ProvenTransaction,
461 transaction_inputs: impl Into<TransactionInputs>,
462 ) -> Result<BlockNumber, ClientError> {
463 info!("Submitting transaction to the network...");
464 let block_num = self
465 .rpc_api
466 .submit_proven_transaction(proven_transaction, transaction_inputs.into())
467 .await?;
468 info!("Transaction submitted.");
469
470 Ok(block_num)
471 }
472
473 pub async fn get_transaction_store_update(
476 &self,
477 tx_result: &TransactionResult,
478 submission_height: BlockNumber,
479 ) -> Result<TransactionStoreUpdate, TransactionStoreUpdateError> {
480 let note_updates = self.get_note_updates(submission_height, tx_result).await?;
481
482 let mut new_tags: Vec<NoteTagRecord> = note_updates
483 .updated_input_notes()
484 .filter_map(|note| {
485 let note = note.inner();
486
487 if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) =
488 note.state()
489 {
490 Some(NoteTagRecord::with_note_source(*tag, note.details_commitment()))
491 } else {
492 None
493 }
494 })
495 .collect();
496
497 new_tags.extend(note_updates.updated_output_notes().map(|note| {
498 let note = note.inner();
499 NoteTagRecord::with_note_source(note.metadata().tag(), note.details_commitment())
500 }));
501
502 Ok(TransactionStoreUpdate::new(
503 tx_result.executed_transaction().clone(),
504 submission_height,
505 note_updates,
506 tx_result.future_notes().to_vec(),
507 new_tags,
508 ))
509 }
510
511 pub async fn apply_transaction(
514 &self,
515 tx_result: &TransactionResult,
516 submission_height: BlockNumber,
517 ) -> Result<(), ClientError> {
518 let tx_update = self.get_transaction_store_update(tx_result, submission_height).await?;
519
520 self.apply_transaction_update(tx_update).await?;
521
522 for observer in &self.transaction_observers {
524 if let Err(err) = observer.apply(tx_result).await {
525 tracing::warn!(
526 observer = observer.name(),
527 error = ?err,
528 "TransactionObserver::apply failed; continuing with remaining observers",
529 );
530 }
531 }
532
533 Ok(())
534 }
535
536 pub async fn apply_transaction_update(
537 &self,
538 tx_update: TransactionStoreUpdate,
539 ) -> Result<(), ClientError> {
540 info!("Applying transaction to the local store...");
543
544 let executed_transaction = tx_update.executed_transaction();
545 let account_id = executed_transaction.account_id();
546
547 if self.account_reader(account_id).status().await?.is_locked() {
548 return Err(ClientError::AccountLocked(account_id));
549 }
550
551 self.store.apply_transaction(tx_update).await?;
552 info!("Transaction stored.");
553 Ok(())
554 }
555
556 pub async fn execute_program(
561 &mut self,
562 account_id: AccountId,
563 tx_script: TransactionScript,
564 advice_inputs: AdviceInputs,
565 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
566 ) -> Result<[Felt; MIN_STACK_DEPTH], ClientError> {
567 let (data_store, block_ref) =
568 self.prepare_program_execution(account_id, foreign_accounts).await?;
569
570 Ok(self
571 .build_executor(&data_store)?
572 .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
573 .await?)
574 }
575
576 #[cfg(feature = "dap")]
579 pub async fn execute_program_with_dap(
580 &mut self,
581 account_id: AccountId,
582 tx_script: TransactionScript,
583 advice_inputs: AdviceInputs,
584 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
585 ) -> Result<[Felt; MIN_STACK_DEPTH], ClientError> {
586 let (data_store, block_ref) =
587 self.prepare_program_execution(account_id, foreign_accounts).await?;
588
589 Ok(self
590 .build_dap_executor(&data_store)?
591 .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
592 .await?)
593 }
594
595 pub async fn validate_request(
605 &self,
606 account_id: AccountId,
607 transaction_request: &TransactionRequest,
608 ) -> Result<(), ClientError> {
609 self.validate_recency().await?;
610 validate_output_note_senders(transaction_request, account_id)?;
611 let account = self.try_get_account(account_id).await?;
612 validate_account_request(transaction_request, &account)
613 }
614
615 async fn validate_recency(&self) -> Result<(), ClientError> {
616 if let Some(max_block_number_delta) = self.max_block_number_delta {
617 let current_chain_tip =
618 self.rpc_api.get_block_header_by_number(None, false).await?.0.block_num();
619
620 if current_chain_tip > self.store.get_sync_height().await? + max_block_number_delta {
621 return Err(ClientError::RecencyConditionError(
622 "The client is too far behind the chain tip to execute the transaction",
623 ));
624 }
625 }
626 Ok(())
627 }
628
629 pub async fn ensure_ntx_scripts_registered(
642 &mut self,
643 account_id: AccountId,
644 scripts: &[NoteScript],
645 tx_prover: Arc<dyn TransactionProver>,
646 ) -> Result<(), ClientError> {
647 let mut missing_scripts = Vec::new();
648
649 for script in scripts {
650 if StandardNote::from_script(script).is_some() {
652 continue;
653 }
654
655 let script_root = script.root();
656
657 match self.rpc_api.get_note_script_by_root(script_root.into()).await {
659 Ok(Some(_)) => {},
660 Ok(None) => missing_scripts.push(script.clone()),
661 Err(source) => {
662 return Err(ClientError::NtxScriptRegistrationFailed {
663 script_root: script_root.into(),
664 source,
665 });
666 },
667 }
668 }
669
670 if missing_scripts.is_empty() {
671 return Ok(());
672 }
673
674 let registration_request = TransactionRequestBuilder::new().build_register_note_scripts(
675 account_id,
676 missing_scripts,
677 self.rng(),
678 )?;
679
680 let tx_result = self.execute_transaction(account_id, registration_request).await?;
681 let proven = self.prove_transaction_with(&tx_result, tx_prover).await?;
682 let submission_height = self.submit_proven_transaction(proven, &tx_result).await?;
683 self.apply_transaction(&tx_result, submission_height).await?;
684
685 Ok(())
686 }
687
688 pub(crate) async fn get_valid_input_notes(
694 &self,
695 account: &Account,
696 mut input_notes: InputNotes<InputNote>,
697 tx_args: TransactionArgs,
698 output_recipients: &[NoteRecipient],
699 ) -> Result<InputNotes<InputNote>, ClientError> {
700 loop {
701 let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
702 data_store.register_note_scripts(output_recipients.iter().map(|r| r.script().clone()));
703
704 data_store.mast_store().load_account_code(account.code());
705 let execution = NoteConsumptionChecker::new(&self.build_executor(&data_store)?)
706 .check_notes_consumability(
707 account.id(),
708 self.store.get_sync_height().await?,
709 input_notes.iter().map(|n| n.clone().into_note()).collect(),
710 tx_args.clone(),
711 )
712 .await?;
713
714 if execution.failed().is_empty() {
715 break;
716 }
717
718 let failed_note_ids: BTreeSet<NoteId> =
719 execution.failed().iter().map(|n| n.note().id()).collect();
720 let filtered_input_notes = InputNotes::new(
721 input_notes
722 .into_iter()
723 .filter(|note| !failed_note_ids.contains(¬e.id()))
724 .collect(),
725 )
726 .expect("Created from a valid input notes list");
727
728 input_notes = filtered_input_notes;
729 }
730
731 Ok(input_notes)
732 }
733
734 async fn retrieve_foreign_account_inputs(
741 &self,
742 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
743 ) -> Result<(Option<BlockNumber>, Vec<AccountInputs>), ClientError> {
744 if foreign_accounts.is_empty() {
745 return Ok((None, Vec::new()));
746 }
747
748 let block_num = self.store.get_sync_height().await?;
749 let mut return_foreign_account_inputs = Vec::with_capacity(foreign_accounts.len());
750
751 for foreign_account in foreign_accounts.into_values() {
752 let foreign_account_inputs = match foreign_account {
753 ForeignAccount::Public(account_id, storage_requirements) => {
754 fetch_public_account_inputs(
755 &self.store,
756 &self.rpc_api,
757 account_id,
758 storage_requirements,
759 AccountStateAt::Block(block_num),
760 )
761 .await?
762 },
763 ForeignAccount::Private(partial_account) => {
764 let account_id = partial_account.id();
765 let (_, account_proof) = self
766 .rpc_api
767 .get_account(
768 account_id,
769 GetAccountRequest::new().at(AccountStateAt::Block(block_num)),
770 )
771 .await?;
772 let (witness, _) = account_proof.into_parts();
773 AccountInputs::new(partial_account, witness)
774 },
775 };
776
777 return_foreign_account_inputs.push(foreign_account_inputs);
778 }
779
780 Ok((Some(block_num), return_foreign_account_inputs))
781 }
782
783 async fn prepare_program_execution(
787 &mut self,
788 account_id: AccountId,
789 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
790 ) -> Result<(ClientDataStore, BlockNumber), ClientError> {
791 let (fpi_block_number, foreign_account_inputs) =
792 self.retrieve_foreign_account_inputs(foreign_accounts).await?;
793
794 let block_ref = if let Some(block_number) = fpi_block_number {
795 block_number
796 } else {
797 self.get_sync_height().await?
798 };
799
800 let account_record = self
801 .store
802 .get_account(account_id)
803 .await?
804 .ok_or(ClientError::AccountDataNotFound(account_id))?;
805
806 let account: Account = account_record.try_into()?;
807
808 let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
809
810 data_store.mast_store().load_account_code(account.code());
812
813 for fpi_account in &foreign_account_inputs {
814 data_store.mast_store().load_account_code(fpi_account.code());
815 }
816
817 data_store.register_foreign_account_inputs(foreign_account_inputs);
818
819 Ok((data_store, block_ref))
820 }
821
822 pub(crate) fn build_executor<'store, 'auth, STORE: DataStore + Sync>(
825 &'auth self,
826 data_store: &'store STORE,
827 ) -> Result<TransactionExecutor<'store, 'auth, STORE, AUTH>, TransactionExecutorError> {
828 let mut executor = TransactionExecutor::new(data_store)
829 .with_options(self.exec_options)?
830 .with_source_manager(self.source_manager.clone());
831 if let Some(authenticator) = self.authenticator.as_deref() {
832 executor = executor.with_authenticator(authenticator);
833 }
834 Ok(executor)
835 }
836
837 async fn get_native_account_record(
840 &self,
841 account_id: AccountId,
842 ) -> Result<AccountRecord, ClientError> {
843 let account_record = self
844 .store
845 .get_account(account_id)
846 .await?
847 .ok_or(ClientError::AccountDataNotFound(account_id))?;
848 if account_record.is_watched() {
849 return Err(ClientError::AccountIsWatched(account_id));
850 }
851 Ok(account_record)
852 }
853
854 #[cfg(feature = "dap")]
856 pub(crate) fn build_dap_executor<'store, 'auth, STORE: DataStore + Sync>(
857 &'auth self,
858 data_store: &'store STORE,
859 ) -> Result<
860 TransactionExecutor<'store, 'auth, STORE, AUTH, dap_executor::DapProgramExecutor>,
861 TransactionExecutorError,
862 > {
863 Ok(self
864 .build_executor(data_store)?
865 .with_program_executor::<dap_executor::DapProgramExecutor>())
866 }
867
868 async fn get_note_updates(
871 &self,
872 submission_height: BlockNumber,
873 tx_result: &TransactionResult,
874 ) -> Result<NoteUpdateTracker, TransactionStoreUpdateError> {
875 let executed_tx = tx_result.executed_transaction();
876 let current_timestamp = self.store.get_current_timestamp();
877 let current_block_num = self.store.get_sync_height().await?;
878
879 let new_output_notes = executed_tx
881 .output_notes()
882 .iter()
883 .cloned()
884 .filter_map(|output_note| {
885 OutputNoteRecord::try_from_output_note(output_note, submission_height).ok()
886 })
887 .collect::<Vec<_>>();
888
889 let mut new_input_notes = vec![];
891 let output_notes: Vec<Note> =
892 notes_from_output(executed_tx.output_notes()).cloned().collect();
893 let note_screener = self.note_screener().clone();
894 let output_note_relevances = note_screener.can_consume_batch(&output_notes).await?;
895
896 for note in output_notes {
897 if output_note_relevances.contains_key(¬e.id()) {
898 let metadata = *note.metadata();
899 let tag = metadata.tag();
900 let attachments = note.attachments().clone();
901
902 new_input_notes.push(InputNoteRecord::new(
903 note.into(),
904 attachments,
905 current_timestamp,
906 ExpectedNoteState {
907 metadata: Some(metadata),
908 after_block_num: submission_height,
909 tag: Some(tag),
910 }
911 .into(),
912 ));
913 }
914 }
915
916 new_input_notes.extend(tx_result.future_notes().iter().map(|(note_details, tag)| {
918 InputNoteRecord::new(
919 note_details.clone(),
920 NoteAttachments::empty(),
921 None,
922 ExpectedNoteState {
923 metadata: None,
924 after_block_num: current_block_num,
925 tag: Some(*tag),
926 }
927 .into(),
928 )
929 }));
930
931 let consumed_note_ids =
936 executed_tx.tx_inputs().input_notes().iter().map(InputNote::id).collect();
937
938 let consumed_notes =
939 self.store.get_input_notes(NoteFilter::List(consumed_note_ids)).await?;
940
941 let tracked_note_ids =
942 consumed_notes.iter().filter_map(InputNoteRecord::id).collect::<BTreeSet<_>>();
943
944 for input_note in executed_tx.tx_inputs().input_notes() {
945 if !tracked_note_ids.contains(&input_note.id()) {
946 let mut input_note_record = InputNoteRecord::from(input_note.clone());
947 input_note_record.consumed_locally(
948 executed_tx.account_id(),
949 executed_tx.id(),
950 current_timestamp,
951 )?;
952 new_input_notes.push(input_note_record);
953 }
954 }
955
956 let mut updated_input_notes = vec![];
957
958 for mut input_note_record in consumed_notes {
959 if input_note_record.consumed_locally(
960 executed_tx.account_id(),
961 executed_tx.id(),
962 current_timestamp,
963 )? {
964 updated_input_notes.push(input_note_record);
965 }
966 }
967
968 Ok(NoteUpdateTracker::for_transaction_updates(
969 new_input_notes,
970 updated_input_notes,
971 new_output_notes,
972 ))
973 }
974}
975
976#[derive(Debug, thiserror::Error)]
982pub enum TransactionStoreUpdateError {
983 #[error("store error")]
984 Store(#[from] StoreError),
985 #[error("note screener error")]
986 NoteScreener(#[from] NoteScreenerError),
987 #[error("note record error")]
988 NoteRecord(#[from] NoteRecordError),
989}
990
991pub(crate) struct PreparedTransaction {
996 pub(crate) notes: InputNotes<InputNote>,
997 pub(crate) output_recipients: Vec<NoteRecipient>,
998 pub(crate) future_notes: Vec<(NoteDetails, NoteTag)>,
999 pub(crate) tx_args: TransactionArgs,
1000 pub(crate) foreign_account_inputs: Vec<AccountInputs>,
1001 pub(crate) block_num: BlockNumber,
1002 pub(crate) ignore_invalid_notes: bool,
1003}
1004
1005impl PreparedTransaction {
1006 pub(crate) fn output_note_scripts(&self) -> impl Iterator<Item = NoteScript> + '_ {
1009 self.output_recipients.iter().map(|recipient| recipient.script().clone())
1010 }
1011}
1012
1013fn get_outgoing_assets(
1018 transaction_request: &TransactionRequest,
1019) -> (BTreeMap<AccountId, u64>, Vec<NonFungibleAsset>) {
1020 let mut own_notes_assets = match transaction_request.script_template() {
1022 Some(TransactionScriptTemplate::SendNotes(notes)) => notes
1023 .iter()
1024 .map(|note| (note.id(), note.assets().clone()))
1025 .collect::<BTreeMap<_, _>>(),
1026 _ => BTreeMap::default(),
1027 };
1028 let mut output_notes_assets = transaction_request
1030 .expected_output_own_notes()
1031 .into_iter()
1032 .map(|note| (note.id(), note.assets().clone()))
1033 .collect::<BTreeMap<_, _>>();
1034
1035 output_notes_assets.append(&mut own_notes_assets);
1037
1038 let outgoing_assets = output_notes_assets.values().flat_map(|note_assets| note_assets.iter());
1040
1041 request::collect_assets(outgoing_assets)
1042}
1043
1044pub(super) fn validate_account_request(
1048 transaction_request: &TransactionRequest,
1049 account: &Account,
1050) -> Result<(), ClientError> {
1051 if account.code_interface().contains([FungibleFaucet::mint_and_send_root()]) {
1052 Ok(())
1054 } else {
1055 validate_basic_account_request(transaction_request, account)
1056 }
1057}
1058
1059fn validate_output_note_senders(
1067 transaction_request: &TransactionRequest,
1068 account_id: AccountId,
1069) -> Result<(), ClientError> {
1070 for note in transaction_request.expected_output_own_notes() {
1071 let sender = note.metadata().sender();
1072 if sender != account_id {
1073 return Err(ClientError::TransactionRequestError(
1074 TransactionRequestError::OutputNoteSenderMismatch {
1075 expected: account_id,
1076 actual: sender,
1077 },
1078 ));
1079 }
1080 }
1081
1082 Ok(())
1083}
1084
1085fn validate_basic_account_request(
1088 transaction_request: &TransactionRequest,
1089 account: &Account,
1090) -> Result<(), ClientError> {
1091 let (fungible_balance_map, non_fungible_set) = get_outgoing_assets(transaction_request);
1093
1094 let (incoming_fungible_balance_map, incoming_non_fungible_balance_set) =
1096 transaction_request.incoming_assets();
1097
1098 let mut available_fungible: BTreeMap<AccountId, u64> = BTreeMap::new();
1101 for asset in account.vault().assets() {
1102 if let Asset::Fungible(fungible) = asset {
1103 let balance = available_fungible.entry(fungible.faucet_id()).or_default();
1104 *balance = balance.saturating_add(fungible.amount().as_u64());
1105 }
1106 }
1107
1108 for (faucet_id, amount) in fungible_balance_map {
1111 let account_asset_amount = available_fungible.get(&faucet_id).copied().unwrap_or(0);
1112 let incoming_balance = incoming_fungible_balance_map.get(&faucet_id).unwrap_or(&0);
1113 if account_asset_amount + incoming_balance < amount {
1114 return Err(ClientError::AssetError(AssetError::FungibleAssetAmountNotSufficient {
1115 minuend: account_asset_amount,
1116 subtrahend: amount,
1117 }));
1118 }
1119 }
1120
1121 for non_fungible in &non_fungible_set {
1124 match account.vault().has_non_fungible_asset(*non_fungible) {
1125 Ok(true) => (),
1126 Ok(false) => {
1127 if !incoming_non_fungible_balance_set.contains(non_fungible) {
1129 return Err(ClientError::TransactionRequestError(
1130 TransactionRequestError::MissingNonFungibleAsset(non_fungible.faucet_id()),
1131 ));
1132 }
1133 },
1134 _ => {
1135 return Err(ClientError::TransactionRequestError(
1136 TransactionRequestError::MissingNonFungibleAsset(non_fungible.faucet_id()),
1137 ));
1138 },
1139 }
1140 }
1141
1142 Ok(())
1143}
1144
1145pub(crate) async fn fetch_public_account_inputs(
1152 store: &Arc<dyn Store>,
1153 rpc_api: &Arc<dyn NodeRpcClient>,
1154 account_id: AccountId,
1155 storage_requirements: AccountStorageRequirements,
1156 account_state_at: AccountStateAt,
1157) -> Result<AccountInputs, ClientError> {
1158 let known_code: Option<AccountCode> =
1159 store.get_foreign_account_code(vec![account_id]).await?.into_values().next();
1160
1161 let vault = store
1162 .get_account_header(account_id)
1163 .await?
1164 .map_or(VaultFetch::Always, |(header, ..)| {
1165 VaultFetch::IfChangedFrom(header.vault_root())
1166 });
1167
1168 let (block_num, mut account_proof) = rpc_api
1169 .get_account(
1170 account_id,
1171 GetAccountRequest::new()
1172 .with_storage(StorageMapFetch::Slots(storage_requirements.clone()))
1173 .at(account_state_at)
1174 .with_known_code(known_code)
1175 .with_vault(vault),
1176 )
1177 .await?;
1178
1179 if let Some(details) = account_proof.details_mut() {
1180 rpc_api.resolve_oversize_vault(account_id, block_num, details).await?;
1181 rpc_api.resolve_oversize_storage_maps(account_id, block_num, details).await?;
1182 }
1183
1184 let account_inputs = request::account_proof_into_inputs(account_proof, &storage_requirements)?;
1185
1186 let _ = store
1187 .upsert_foreign_account_code(account_id, account_inputs.code().clone())
1188 .await
1189 .inspect_err(|err| {
1190 tracing::warn!(
1191 %account_id,
1192 %err,
1193 "Failed to persist foreign account code to store"
1194 );
1195 });
1196
1197 Ok(account_inputs)
1198}
1199
1200pub fn notes_from_output(output_notes: &RawOutputNotes) -> impl Iterator<Item = &Note> {
1205 output_notes.iter().filter_map(|n| match n {
1206 RawOutputNote::Full(n) => Some(n),
1207 RawOutputNote::Partial(_) => None,
1208 })
1209}
1210
1211pub(crate) fn validate_executed_transaction(
1214 executed_transaction: &ExecutedTransaction,
1215 expected_output_recipients: &[NoteRecipient],
1216) -> Result<(), ClientError> {
1217 let tx_output_recipient_digests = executed_transaction
1218 .output_notes()
1219 .iter()
1220 .filter_map(|n| n.recipient().map(NoteRecipient::digest))
1221 .collect::<Vec<_>>();
1222
1223 let missing_recipient_digest: Vec<Word> = expected_output_recipients
1224 .iter()
1225 .filter_map(|recipient| {
1226 (!tx_output_recipient_digests.contains(&recipient.digest()))
1227 .then_some(recipient.digest())
1228 })
1229 .collect();
1230
1231 if !missing_recipient_digest.is_empty() {
1232 return Err(ClientError::MissingOutputRecipients(missing_recipient_digest));
1233 }
1234
1235 Ok(())
1236}
1237
1238#[cfg(test)]
1242mod tests {
1243 use alloc::vec;
1244
1245 use miden_protocol::Word;
1246 use miden_protocol::account::AccountId;
1247 use miden_protocol::asset::FungibleAsset;
1248 use miden_protocol::crypto::rand::RandomCoin;
1249 use miden_protocol::note::{Note, NoteType};
1250 use miden_protocol::testing::account_id::{
1251 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
1252 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
1253 ACCOUNT_ID_SENDER,
1254 };
1255 use miden_standards::note::P2idNote;
1256
1257 use super::{TransactionRequestBuilder, validate_output_note_senders};
1258 use crate::ClientError;
1259 use crate::transaction::TransactionRequestError;
1260
1261 fn own_note_with_sender(sender: AccountId) -> Note {
1262 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
1263 let target_id =
1264 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1265 let mut rng = RandomCoin::new(Word::default());
1266
1267 P2idNote::builder()
1268 .sender(sender)
1269 .target(target_id)
1270 .asset(FungibleAsset::new(faucet_id, 100).unwrap())
1271 .note_type(NoteType::Public)
1272 .generate_serial_number(&mut rng)
1273 .build()
1274 .expect("note creation failed")
1275 .into()
1276 }
1277
1278 #[test]
1279 fn output_note_with_foreign_sender_is_rejected() {
1280 let account_id =
1281 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1282 let foreign_sender = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
1283 assert_ne!(account_id, foreign_sender);
1284
1285 let request = TransactionRequestBuilder::new()
1286 .own_output_notes(vec![own_note_with_sender(foreign_sender)])
1287 .build()
1288 .unwrap();
1289
1290 let err = validate_output_note_senders(&request, account_id).unwrap_err();
1291 match err {
1292 ClientError::TransactionRequestError(
1293 TransactionRequestError::OutputNoteSenderMismatch { expected, actual },
1294 ) => {
1295 assert_eq!(expected, account_id);
1296 assert_eq!(actual, foreign_sender);
1297 },
1298 other => panic!("expected OutputNoteSenderMismatch, got {other:?}"),
1299 }
1300 }
1301
1302 #[test]
1303 fn output_note_with_matching_sender_is_accepted() {
1304 let account_id =
1305 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1306
1307 let request = TransactionRequestBuilder::new()
1308 .own_output_notes(vec![own_note_with_sender(account_id)])
1309 .build()
1310 .unwrap();
1311
1312 validate_output_note_senders(&request, account_id).unwrap();
1313 }
1314
1315 #[test]
1316 fn request_without_own_output_notes_is_accepted() {
1317 let account_id =
1318 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1319 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
1320
1321 let request = TransactionRequestBuilder::new()
1323 .input_notes(vec![(own_note_with_sender(faucet_id), None)])
1324 .build()
1325 .unwrap();
1326
1327 validate_output_note_senders(&request, account_id).unwrap();
1328 }
1329}