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::interface::AccountInterfaceExt;
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_tx::auth::TransactionAuthenticator;
176pub use miden_tx::{
177 DataStoreError,
178 LocalTransactionProver,
179 ProvingOptions,
180 TransactionExecutorError,
181 TransactionProverError,
182};
183pub use result::TransactionResult;
184
185impl<AUTH> Client<AUTH>
187where
188 AUTH: TransactionAuthenticator + Sync + 'static,
189{
190 pub async fn get_transactions(
195 &self,
196 filter: TransactionFilter,
197 ) -> Result<Vec<TransactionRecord>, ClientError> {
198 self.store.get_transactions(filter).await.map_err(Into::into)
199 }
200
201 pub fn new_transaction_batch(&self) -> BatchBuilder<'_, AUTH> {
209 let inner_data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
210 BatchBuilder {
211 client: self,
212 data_store: InMemoryBatchDataStore::new(inner_data_store),
213 pushed_txs: Vec::new(),
214 consumed_input_notes: BTreeSet::new(),
215 }
216 }
217
218 pub async fn submit_new_transaction(
227 &mut self,
228 account_id: AccountId,
229 transaction_request: TransactionRequest,
230 ) -> Result<TransactionId, ClientError> {
231 let prover = self.tx_prover.clone();
232 self.submit_new_transaction_with_prover(account_id, transaction_request, prover)
233 .await
234 }
235
236 pub async fn submit_new_transaction_with_prover(
243 &mut self,
244 account_id: AccountId,
245 transaction_request: TransactionRequest,
246 tx_prover: Arc<dyn TransactionProver>,
247 ) -> Result<TransactionId, ClientError> {
248 if !transaction_request.expected_ntx_scripts().is_empty() {
251 Box::pin(self.ensure_ntx_scripts_registered(
252 account_id,
253 transaction_request.expected_ntx_scripts(),
254 tx_prover.clone(),
255 ))
256 .await?;
257 }
258
259 let tx_result = self.execute_transaction(account_id, transaction_request).await?;
260 let tx_id = tx_result.executed_transaction().id();
261
262 let proven_transaction = self.prove_transaction_with(&tx_result, tx_prover).await?;
263 let submission_height =
264 self.submit_proven_transaction(proven_transaction, &tx_result).await?;
265
266 let tx_update =
275 Box::new(self.get_transaction_store_update(&tx_result, submission_height).await?);
276
277 if let Err(apply_err) = self.apply_transaction_update((*tx_update).clone()).await {
278 info!(
279 "apply_transaction_update failed for submitted tx {tx_id}; returning \
280 ApplyTransactionAfterSubmitFailed with the pending update attached: {apply_err}"
281 );
282 return Err(ClientError::ApplyTransactionAfterSubmitFailed {
283 pending_update: tx_update,
284 source: Box::new(apply_err),
285 });
286 }
287
288 for observer in &self.transaction_observers {
292 crate::errors::log_observer_failure(
293 observer.name(),
294 "TransactionObserver::apply",
295 observer.apply(&tx_result).await,
296 );
297 }
298
299 Ok(tx_id)
300 }
301
302 pub async fn execute_transaction(
312 &mut self,
313 account_id: AccountId,
314 transaction_request: TransactionRequest,
315 ) -> Result<TransactionResult, ClientError> {
316 let account: Account = self.get_native_account_record(account_id).await?.try_into()?;
317
318 let prep = self.prepare_transaction(&account, transaction_request).await?;
319
320 let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
321 data_store.register_note_scripts(prep.output_note_scripts());
322 for fpi_account in &prep.foreign_account_inputs {
323 data_store.mast_store().load_account_code(fpi_account.code());
324 }
325 data_store.register_foreign_account_inputs(prep.foreign_account_inputs);
326
327 data_store.mast_store().load_account_code(account.code());
328
329 let mut notes = prep.notes;
330 if prep.ignore_invalid_notes {
331 notes = self
332 .get_valid_input_notes(
333 &account,
334 notes,
335 prep.tx_args.clone(),
336 &prep.output_recipients,
337 )
338 .await?;
339 }
340
341 let executed_transaction = self
342 .build_executor(&data_store)?
343 .execute_transaction(account_id, prep.block_num, notes, prep.tx_args)
344 .await?;
345
346 validate_executed_transaction(&executed_transaction, &prep.output_recipients)?;
347 TransactionResult::new(executed_transaction, prep.future_notes)
348 }
349
350 pub(crate) async fn prepare_transaction(
362 &self,
363 account: &Account,
364 transaction_request: TransactionRequest,
365 ) -> Result<PreparedTransaction, ClientError> {
366 let account_id = account.id();
367 self.validate_recency().await?;
368 validate_account_request(&transaction_request, account)?;
369
370 let mut stored_note_records = self
372 .store
373 .get_input_notes(NoteFilter::List(transaction_request.input_note_ids().collect()))
374 .await?;
375
376 for note in &stored_note_records {
378 if note.is_consumed() {
379 let id = note.id().expect(
380 "stored note records reaching this check carry metadata so id() is Some",
381 );
382 return Err(ClientError::TransactionRequestError(
383 TransactionRequestError::InputNoteAlreadyConsumed(id),
384 ));
385 }
386 }
387
388 stored_note_records.retain(InputNoteRecord::is_authenticated);
390
391 let notes = transaction_request.build_input_notes(stored_note_records)?;
392
393 let output_recipients =
394 transaction_request.expected_output_recipients().cloned().collect::<Vec<_>>();
395
396 let future_notes: Vec<(NoteDetails, NoteTag)> =
397 transaction_request.expected_future_notes().cloned().collect();
398
399 let tx_script = transaction_request
400 .build_transaction_script(&self.get_account_interface(account_id).await?)?;
401
402 let foreign_accounts = transaction_request.foreign_accounts().clone();
403
404 let (fpi_block_num, foreign_account_inputs) =
405 self.retrieve_foreign_account_inputs(foreign_accounts).await?;
406
407 let ignore_invalid_notes = transaction_request.ignore_invalid_input_notes();
408
409 let block_num = if let Some(block_num) = fpi_block_num {
410 block_num
411 } else {
412 self.store.get_sync_height().await?
413 };
414
415 let tx_args = transaction_request.into_transaction_args(tx_script);
416
417 Ok(PreparedTransaction {
418 notes,
419 output_recipients,
420 future_notes,
421 tx_args,
422 foreign_account_inputs,
423 block_num,
424 ignore_invalid_notes,
425 })
426 }
427
428 pub async fn prove_transaction(
430 &self,
431 tx_result: &TransactionResult,
432 ) -> Result<ProvenTransaction, ClientError> {
433 self.prove_transaction_with(tx_result, self.tx_prover.clone()).await
434 }
435
436 pub async fn prove_transaction_with(
438 &self,
439 tx_result: &TransactionResult,
440 tx_prover: Arc<dyn TransactionProver>,
441 ) -> Result<ProvenTransaction, ClientError> {
442 info!("Proving transaction...");
443
444 let proven_transaction =
445 tx_prover.prove(tx_result.executed_transaction().clone().into()).await?;
446
447 info!("Transaction proven.");
448
449 Ok(proven_transaction)
450 }
451
452 pub async fn submit_proven_transaction(
455 &mut self,
456 proven_transaction: ProvenTransaction,
457 transaction_inputs: impl Into<TransactionInputs>,
458 ) -> Result<BlockNumber, ClientError> {
459 info!("Submitting transaction to the network...");
460 let block_num = self
461 .rpc_api
462 .submit_proven_transaction(proven_transaction, transaction_inputs.into())
463 .await?;
464 info!("Transaction submitted.");
465
466 Ok(block_num)
467 }
468
469 pub async fn get_transaction_store_update(
472 &self,
473 tx_result: &TransactionResult,
474 submission_height: BlockNumber,
475 ) -> Result<TransactionStoreUpdate, TransactionStoreUpdateError> {
476 let note_updates = self.get_note_updates(submission_height, tx_result).await?;
477
478 let new_tags: Vec<NoteTagRecord> = note_updates
481 .updated_input_notes()
482 .filter_map(|note| {
483 let note = note.inner();
484
485 if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) =
486 note.state()
487 {
488 Some(NoteTagRecord::with_note_source(*tag, note.details_commitment()))
489 } else {
490 None
491 }
492 })
493 .collect();
494
495 Ok(TransactionStoreUpdate::new(
496 tx_result.executed_transaction().clone(),
497 submission_height,
498 note_updates,
499 tx_result.future_notes().to_vec(),
500 new_tags,
501 ))
502 }
503
504 pub async fn apply_transaction(
507 &self,
508 tx_result: &TransactionResult,
509 submission_height: BlockNumber,
510 ) -> Result<(), ClientError> {
511 let tx_update = self.get_transaction_store_update(tx_result, submission_height).await?;
512
513 self.apply_transaction_update(tx_update).await?;
514
515 for observer in &self.transaction_observers {
517 if let Err(err) = observer.apply(tx_result).await {
518 tracing::warn!(
519 observer = observer.name(),
520 error = ?err,
521 "TransactionObserver::apply failed; continuing with remaining observers",
522 );
523 }
524 }
525
526 Ok(())
527 }
528
529 pub async fn apply_transaction_update(
530 &self,
531 tx_update: TransactionStoreUpdate,
532 ) -> Result<(), ClientError> {
533 info!("Applying transaction to the local store...");
536
537 let executed_transaction = tx_update.executed_transaction();
538 let account_id = executed_transaction.account_id();
539
540 if self.account_reader(account_id).status().await?.is_locked() {
541 return Err(ClientError::AccountLocked(account_id));
542 }
543
544 self.store.apply_transaction(tx_update).await?;
545 info!("Transaction stored.");
546 Ok(())
547 }
548
549 pub async fn execute_program(
554 &mut self,
555 account_id: AccountId,
556 tx_script: TransactionScript,
557 advice_inputs: AdviceInputs,
558 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
559 ) -> Result<[Felt; MIN_STACK_DEPTH], ClientError> {
560 let (data_store, block_ref) =
561 self.prepare_program_execution(account_id, foreign_accounts).await?;
562
563 Ok(self
564 .build_executor(&data_store)?
565 .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
566 .await?)
567 }
568
569 #[cfg(feature = "dap")]
572 pub async fn execute_program_with_dap(
573 &mut self,
574 account_id: AccountId,
575 tx_script: TransactionScript,
576 advice_inputs: AdviceInputs,
577 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
578 ) -> Result<[Felt; MIN_STACK_DEPTH], ClientError> {
579 let (data_store, block_ref) =
580 self.prepare_program_execution(account_id, foreign_accounts).await?;
581
582 Ok(self
583 .build_dap_executor(&data_store)?
584 .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
585 .await?)
586 }
587
588 pub async fn validate_request(
598 &self,
599 account_id: AccountId,
600 transaction_request: &TransactionRequest,
601 ) -> Result<(), ClientError> {
602 self.validate_recency().await?;
603 validate_output_note_senders(transaction_request, account_id)?;
604 let account = self.try_get_account(account_id).await?;
605 validate_account_request(transaction_request, &account)
606 }
607
608 async fn validate_recency(&self) -> Result<(), ClientError> {
609 if let Some(max_block_number_delta) = self.max_block_number_delta {
610 let current_chain_tip =
611 self.rpc_api.get_block_header_by_number(None, false).await?.0.block_num();
612
613 if current_chain_tip > self.store.get_sync_height().await? + max_block_number_delta {
614 return Err(ClientError::RecencyConditionError(
615 "The client is too far behind the chain tip to execute the transaction",
616 ));
617 }
618 }
619 Ok(())
620 }
621
622 pub async fn ensure_ntx_scripts_registered(
635 &mut self,
636 account_id: AccountId,
637 scripts: &[NoteScript],
638 tx_prover: Arc<dyn TransactionProver>,
639 ) -> Result<(), ClientError> {
640 let mut missing_scripts = Vec::new();
641
642 for script in scripts {
643 if StandardNote::from_script(script).is_some() {
645 continue;
646 }
647
648 let script_root = script.root();
649
650 match self.rpc_api.get_note_script_by_root(script_root.into()).await {
652 Ok(Some(_)) => {},
653 Ok(None) => missing_scripts.push(script.clone()),
654 Err(source) => {
655 return Err(ClientError::NtxScriptRegistrationFailed {
656 script_root: script_root.into(),
657 source,
658 });
659 },
660 }
661 }
662
663 if missing_scripts.is_empty() {
664 return Ok(());
665 }
666
667 let registration_request = TransactionRequestBuilder::new().build_register_note_scripts(
668 account_id,
669 missing_scripts,
670 self.rng(),
671 )?;
672
673 let tx_result = self.execute_transaction(account_id, registration_request).await?;
674 let proven = self.prove_transaction_with(&tx_result, tx_prover).await?;
675 let submission_height = self.submit_proven_transaction(proven, &tx_result).await?;
676 self.apply_transaction(&tx_result, submission_height).await?;
677
678 Ok(())
679 }
680
681 pub(crate) async fn get_valid_input_notes(
687 &self,
688 account: &Account,
689 mut input_notes: InputNotes<InputNote>,
690 tx_args: TransactionArgs,
691 output_recipients: &[NoteRecipient],
692 ) -> Result<InputNotes<InputNote>, ClientError> {
693 loop {
694 let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
695 data_store.register_note_scripts(output_recipients.iter().map(|r| r.script().clone()));
696
697 data_store.mast_store().load_account_code(account.code());
698 let execution = NoteConsumptionChecker::new(&self.build_executor(&data_store)?)
699 .check_notes_consumability(
700 account.id(),
701 self.store.get_sync_height().await?,
702 input_notes.iter().map(|n| n.clone().into_note()).collect(),
703 tx_args.clone(),
704 )
705 .await?;
706
707 if execution.failed().is_empty() {
708 break;
709 }
710
711 let failed_note_ids: BTreeSet<NoteId> =
712 execution.failed().iter().map(|n| n.note().id()).collect();
713 let filtered_input_notes = InputNotes::new(
714 input_notes
715 .into_iter()
716 .filter(|note| !failed_note_ids.contains(¬e.id()))
717 .collect(),
718 )
719 .expect("Created from a valid input notes list");
720
721 input_notes = filtered_input_notes;
722 }
723
724 Ok(input_notes)
725 }
726
727 async fn retrieve_foreign_account_inputs(
734 &self,
735 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
736 ) -> Result<(Option<BlockNumber>, Vec<AccountInputs>), ClientError> {
737 if foreign_accounts.is_empty() {
738 return Ok((None, Vec::new()));
739 }
740
741 let block_num = self.store.get_sync_height().await?;
742 let mut return_foreign_account_inputs = Vec::with_capacity(foreign_accounts.len());
743
744 for foreign_account in foreign_accounts.into_values() {
745 let foreign_account_inputs = match foreign_account {
746 ForeignAccount::Public(account_id, storage_requirements) => {
747 fetch_public_account_inputs(
748 &self.store,
749 &self.rpc_api,
750 account_id,
751 storage_requirements,
752 AccountStateAt::Block(block_num),
753 )
754 .await?
755 },
756 ForeignAccount::Private(partial_account) => {
757 let account_id = partial_account.id();
758 let (_, account_proof) = self
759 .rpc_api
760 .get_account(
761 account_id,
762 GetAccountRequest::new().at(AccountStateAt::Block(block_num)),
763 )
764 .await?;
765 let (witness, _) = account_proof.into_parts();
766 AccountInputs::new(partial_account, witness)
767 },
768 };
769
770 return_foreign_account_inputs.push(foreign_account_inputs);
771 }
772
773 Ok((Some(block_num), return_foreign_account_inputs))
774 }
775
776 async fn prepare_program_execution(
780 &mut self,
781 account_id: AccountId,
782 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
783 ) -> Result<(ClientDataStore, BlockNumber), ClientError> {
784 let (fpi_block_number, foreign_account_inputs) =
785 self.retrieve_foreign_account_inputs(foreign_accounts).await?;
786
787 let block_ref = if let Some(block_number) = fpi_block_number {
788 block_number
789 } else {
790 self.get_sync_height().await?
791 };
792
793 let account_record = self
794 .store
795 .get_account(account_id)
796 .await?
797 .ok_or(ClientError::AccountDataNotFound(account_id))?;
798
799 let account: Account = account_record.try_into()?;
800
801 let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
802
803 data_store.mast_store().load_account_code(account.code());
805
806 for fpi_account in &foreign_account_inputs {
807 data_store.mast_store().load_account_code(fpi_account.code());
808 }
809
810 data_store.register_foreign_account_inputs(foreign_account_inputs);
811
812 Ok((data_store, block_ref))
813 }
814
815 pub(crate) fn build_executor<'store, 'auth, STORE: DataStore + Sync>(
818 &'auth self,
819 data_store: &'store STORE,
820 ) -> Result<TransactionExecutor<'store, 'auth, STORE, AUTH>, TransactionExecutorError> {
821 let mut executor = TransactionExecutor::new(data_store)
822 .with_options(self.exec_options)?
823 .with_source_manager(self.source_manager.clone());
824 if let Some(authenticator) = self.authenticator.as_deref() {
825 executor = executor.with_authenticator(authenticator);
826 }
827 Ok(executor)
828 }
829
830 async fn get_native_account_record(
833 &self,
834 account_id: AccountId,
835 ) -> Result<AccountRecord, ClientError> {
836 let account_record = self
837 .store
838 .get_account(account_id)
839 .await?
840 .ok_or(ClientError::AccountDataNotFound(account_id))?;
841 if account_record.is_watched() {
842 return Err(ClientError::AccountIsWatched(account_id));
843 }
844 Ok(account_record)
845 }
846
847 #[cfg(feature = "dap")]
849 pub(crate) fn build_dap_executor<'store, 'auth, STORE: DataStore + Sync>(
850 &'auth self,
851 data_store: &'store STORE,
852 ) -> Result<
853 TransactionExecutor<'store, 'auth, STORE, AUTH, dap_executor::DapProgramExecutor>,
854 TransactionExecutorError,
855 > {
856 Ok(self
857 .build_executor(data_store)?
858 .with_program_executor::<dap_executor::DapProgramExecutor>())
859 }
860
861 pub(crate) async fn get_account_interface(
863 &self,
864 account_id: AccountId,
865 ) -> Result<AccountInterface, ClientError> {
866 let account = self.try_get_account(account_id).await?;
867 Ok(AccountInterface::from_account(&account))
868 }
869
870 async fn get_note_updates(
873 &self,
874 submission_height: BlockNumber,
875 tx_result: &TransactionResult,
876 ) -> Result<NoteUpdateTracker, TransactionStoreUpdateError> {
877 let executed_tx = tx_result.executed_transaction();
878 let current_timestamp = self.store.get_current_timestamp();
879 let current_block_num = self.store.get_sync_height().await?;
880
881 let new_output_notes = executed_tx
883 .output_notes()
884 .iter()
885 .cloned()
886 .filter_map(|output_note| {
887 OutputNoteRecord::try_from_output_note(output_note, submission_height).ok()
888 })
889 .collect::<Vec<_>>();
890
891 let mut new_input_notes = vec![];
893 let output_notes: Vec<Note> =
894 notes_from_output(executed_tx.output_notes()).cloned().collect();
895 let note_screener = self.note_screener().clone();
896 let output_note_relevances = note_screener.can_consume_batch(&output_notes).await?;
897
898 for note in output_notes {
899 if output_note_relevances.contains_key(¬e.id()) {
900 let metadata = *note.metadata();
901 let tag = metadata.tag();
902 let attachments = note.attachments().clone();
903
904 new_input_notes.push(InputNoteRecord::new(
905 note.into(),
906 attachments,
907 current_timestamp,
908 ExpectedNoteState {
909 metadata: Some(metadata),
910 after_block_num: submission_height,
911 tag: Some(tag),
912 }
913 .into(),
914 ));
915 }
916 }
917
918 new_input_notes.extend(tx_result.future_notes().iter().map(|(note_details, tag)| {
920 InputNoteRecord::new(
921 note_details.clone(),
922 NoteAttachments::empty(),
923 None,
924 ExpectedNoteState {
925 metadata: None,
926 after_block_num: current_block_num,
927 tag: Some(*tag),
928 }
929 .into(),
930 )
931 }));
932
933 let consumed_note_ids =
938 executed_tx.tx_inputs().input_notes().iter().map(InputNote::id).collect();
939
940 let consumed_notes =
941 self.store.get_input_notes(NoteFilter::List(consumed_note_ids)).await?;
942
943 let tracked_note_ids =
944 consumed_notes.iter().filter_map(InputNoteRecord::id).collect::<BTreeSet<_>>();
945
946 for input_note in executed_tx.tx_inputs().input_notes() {
947 if !tracked_note_ids.contains(&input_note.id()) {
948 let mut input_note_record = InputNoteRecord::from(input_note.clone());
949 input_note_record.consumed_locally(
950 executed_tx.account_id(),
951 executed_tx.id(),
952 current_timestamp,
953 )?;
954 new_input_notes.push(input_note_record);
955 }
956 }
957
958 let mut updated_input_notes = vec![];
959
960 for mut input_note_record in consumed_notes {
961 if input_note_record.consumed_locally(
962 executed_tx.account_id(),
963 executed_tx.id(),
964 current_timestamp,
965 )? {
966 updated_input_notes.push(input_note_record);
967 }
968 }
969
970 Ok(NoteUpdateTracker::for_transaction_updates(
971 new_input_notes,
972 updated_input_notes,
973 new_output_notes,
974 ))
975 }
976}
977
978#[derive(Debug, thiserror::Error)]
984pub enum TransactionStoreUpdateError {
985 #[error("store error")]
986 Store(#[from] StoreError),
987 #[error("note screener error")]
988 NoteScreener(#[from] NoteScreenerError),
989 #[error("note record error")]
990 NoteRecord(#[from] NoteRecordError),
991}
992
993pub(crate) struct PreparedTransaction {
998 pub(crate) notes: InputNotes<InputNote>,
999 pub(crate) output_recipients: Vec<NoteRecipient>,
1000 pub(crate) future_notes: Vec<(NoteDetails, NoteTag)>,
1001 pub(crate) tx_args: TransactionArgs,
1002 pub(crate) foreign_account_inputs: Vec<AccountInputs>,
1003 pub(crate) block_num: BlockNumber,
1004 pub(crate) ignore_invalid_notes: bool,
1005}
1006
1007impl PreparedTransaction {
1008 pub(crate) fn output_note_scripts(&self) -> impl Iterator<Item = NoteScript> + '_ {
1011 self.output_recipients.iter().map(|recipient| recipient.script().clone())
1012 }
1013}
1014
1015fn get_outgoing_assets(
1020 transaction_request: &TransactionRequest,
1021) -> (BTreeMap<AccountId, u64>, Vec<NonFungibleAsset>) {
1022 let mut own_notes_assets = match transaction_request.script_template() {
1024 Some(TransactionScriptTemplate::SendNotes(notes)) => notes
1025 .iter()
1026 .map(|note| (note.id(), note.assets().clone()))
1027 .collect::<BTreeMap<_, _>>(),
1028 _ => BTreeMap::default(),
1029 };
1030 let mut output_notes_assets = transaction_request
1032 .expected_output_own_notes()
1033 .into_iter()
1034 .map(|note| (note.id(), note.assets().clone()))
1035 .collect::<BTreeMap<_, _>>();
1036
1037 output_notes_assets.append(&mut own_notes_assets);
1039
1040 let outgoing_assets = output_notes_assets.values().flat_map(|note_assets| note_assets.iter());
1042
1043 request::collect_assets(outgoing_assets)
1044}
1045
1046pub(super) fn validate_account_request(
1050 transaction_request: &TransactionRequest,
1051 account: &Account,
1052) -> Result<(), ClientError> {
1053 let account_interface = AccountInterface::from_account(account);
1054 if account_interface
1055 .components()
1056 .contains(&AccountComponentInterface::FungibleFaucet)
1057 {
1058 Ok(())
1060 } else {
1061 validate_basic_account_request(transaction_request, account)
1062 }
1063}
1064
1065fn validate_output_note_senders(
1073 transaction_request: &TransactionRequest,
1074 account_id: AccountId,
1075) -> Result<(), ClientError> {
1076 for note in transaction_request.expected_output_own_notes() {
1077 let sender = note.metadata().sender();
1078 if sender != account_id {
1079 return Err(ClientError::TransactionRequestError(
1080 TransactionRequestError::OutputNoteSenderMismatch {
1081 expected: account_id,
1082 actual: sender,
1083 },
1084 ));
1085 }
1086 }
1087
1088 Ok(())
1089}
1090
1091fn validate_basic_account_request(
1094 transaction_request: &TransactionRequest,
1095 account: &Account,
1096) -> Result<(), ClientError> {
1097 let (fungible_balance_map, non_fungible_set) = get_outgoing_assets(transaction_request);
1099
1100 let (incoming_fungible_balance_map, incoming_non_fungible_balance_set) =
1102 transaction_request.incoming_assets();
1103
1104 let mut available_fungible: BTreeMap<AccountId, u64> = BTreeMap::new();
1107 for asset in account.vault().assets() {
1108 if let Asset::Fungible(fungible) = asset {
1109 let balance = available_fungible.entry(fungible.faucet_id()).or_default();
1110 *balance = balance.saturating_add(fungible.amount().as_u64());
1111 }
1112 }
1113
1114 for (faucet_id, amount) in fungible_balance_map {
1117 let account_asset_amount = available_fungible.get(&faucet_id).copied().unwrap_or(0);
1118 let incoming_balance = incoming_fungible_balance_map.get(&faucet_id).unwrap_or(&0);
1119 if account_asset_amount + incoming_balance < amount {
1120 return Err(ClientError::AssetError(AssetError::FungibleAssetAmountNotSufficient {
1121 minuend: account_asset_amount,
1122 subtrahend: amount,
1123 }));
1124 }
1125 }
1126
1127 for non_fungible in &non_fungible_set {
1130 match account.vault().has_non_fungible_asset(*non_fungible) {
1131 Ok(true) => (),
1132 Ok(false) => {
1133 if !incoming_non_fungible_balance_set.contains(non_fungible) {
1135 return Err(ClientError::TransactionRequestError(
1136 TransactionRequestError::MissingNonFungibleAsset(non_fungible.faucet_id()),
1137 ));
1138 }
1139 },
1140 _ => {
1141 return Err(ClientError::TransactionRequestError(
1142 TransactionRequestError::MissingNonFungibleAsset(non_fungible.faucet_id()),
1143 ));
1144 },
1145 }
1146 }
1147
1148 Ok(())
1149}
1150
1151pub(crate) async fn fetch_public_account_inputs(
1158 store: &Arc<dyn Store>,
1159 rpc_api: &Arc<dyn NodeRpcClient>,
1160 account_id: AccountId,
1161 storage_requirements: AccountStorageRequirements,
1162 account_state_at: AccountStateAt,
1163) -> Result<AccountInputs, ClientError> {
1164 let known_code: Option<AccountCode> =
1165 store.get_foreign_account_code(vec![account_id]).await?.into_values().next();
1166
1167 let vault = store
1168 .get_account_header(account_id)
1169 .await?
1170 .map_or(VaultFetch::Always, |(header, ..)| {
1171 VaultFetch::IfChangedFrom(header.vault_root())
1172 });
1173
1174 let (block_num, mut account_proof) = rpc_api
1175 .get_account(
1176 account_id,
1177 GetAccountRequest::new()
1178 .with_storage(StorageMapFetch::Slots(storage_requirements.clone()))
1179 .at(account_state_at)
1180 .with_known_code(known_code)
1181 .with_vault(vault),
1182 )
1183 .await?;
1184
1185 if let Some(details) = account_proof.details_mut() {
1186 rpc_api.resolve_oversize_vault(account_id, block_num, details).await?;
1187 rpc_api.resolve_oversize_storage_maps(account_id, block_num, details).await?;
1188 }
1189
1190 let account_inputs = request::account_proof_into_inputs(account_proof, &storage_requirements)?;
1191
1192 let _ = store
1193 .upsert_foreign_account_code(account_id, account_inputs.code().clone())
1194 .await
1195 .inspect_err(|err| {
1196 tracing::warn!(
1197 %account_id,
1198 %err,
1199 "Failed to persist foreign account code to store"
1200 );
1201 });
1202
1203 Ok(account_inputs)
1204}
1205
1206pub fn notes_from_output(output_notes: &RawOutputNotes) -> impl Iterator<Item = &Note> {
1211 output_notes.iter().filter_map(|n| match n {
1212 RawOutputNote::Full(n) => Some(n),
1213 RawOutputNote::Partial(_) => None,
1214 })
1215}
1216
1217pub(crate) fn validate_executed_transaction(
1220 executed_transaction: &ExecutedTransaction,
1221 expected_output_recipients: &[NoteRecipient],
1222) -> Result<(), ClientError> {
1223 let tx_output_recipient_digests = executed_transaction
1224 .output_notes()
1225 .iter()
1226 .filter_map(|n| n.recipient().map(NoteRecipient::digest))
1227 .collect::<Vec<_>>();
1228
1229 let missing_recipient_digest: Vec<Word> = expected_output_recipients
1230 .iter()
1231 .filter_map(|recipient| {
1232 (!tx_output_recipient_digests.contains(&recipient.digest()))
1233 .then_some(recipient.digest())
1234 })
1235 .collect();
1236
1237 if !missing_recipient_digest.is_empty() {
1238 return Err(ClientError::MissingOutputRecipients(missing_recipient_digest));
1239 }
1240
1241 Ok(())
1242}
1243
1244#[cfg(test)]
1248mod tests {
1249 use alloc::vec;
1250
1251 use miden_protocol::Word;
1252 use miden_protocol::account::AccountId;
1253 use miden_protocol::asset::FungibleAsset;
1254 use miden_protocol::crypto::rand::RandomCoin;
1255 use miden_protocol::note::{Note, NoteAttachments, NoteType};
1256 use miden_protocol::testing::account_id::{
1257 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
1258 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
1259 ACCOUNT_ID_SENDER,
1260 };
1261 use miden_standards::note::P2idNote;
1262
1263 use super::{TransactionRequestBuilder, validate_output_note_senders};
1264 use crate::ClientError;
1265 use crate::transaction::TransactionRequestError;
1266
1267 fn own_note_with_sender(sender: AccountId) -> Note {
1268 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
1269 let target_id =
1270 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1271 let mut rng = RandomCoin::new(Word::default());
1272
1273 P2idNote::create(
1274 sender,
1275 target_id,
1276 vec![FungibleAsset::new(faucet_id, 100).unwrap().into()],
1277 NoteType::Public,
1278 NoteAttachments::empty(),
1279 &mut rng,
1280 )
1281 .unwrap()
1282 }
1283
1284 #[test]
1285 fn output_note_with_foreign_sender_is_rejected() {
1286 let account_id =
1287 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1288 let foreign_sender = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
1289 assert_ne!(account_id, foreign_sender);
1290
1291 let request = TransactionRequestBuilder::new()
1292 .own_output_notes(vec![own_note_with_sender(foreign_sender)])
1293 .build()
1294 .unwrap();
1295
1296 let err = validate_output_note_senders(&request, account_id).unwrap_err();
1297 match err {
1298 ClientError::TransactionRequestError(
1299 TransactionRequestError::OutputNoteSenderMismatch { expected, actual },
1300 ) => {
1301 assert_eq!(expected, account_id);
1302 assert_eq!(actual, foreign_sender);
1303 },
1304 other => panic!("expected OutputNoteSenderMismatch, got {other:?}"),
1305 }
1306 }
1307
1308 #[test]
1309 fn output_note_with_matching_sender_is_accepted() {
1310 let account_id =
1311 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1312
1313 let request = TransactionRequestBuilder::new()
1314 .own_output_notes(vec![own_note_with_sender(account_id)])
1315 .build()
1316 .unwrap();
1317
1318 validate_output_note_senders(&request, account_id).unwrap();
1319 }
1320
1321 #[test]
1322 fn request_without_own_output_notes_is_accepted() {
1323 let account_id =
1324 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1325 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
1326
1327 let request = TransactionRequestBuilder::new()
1329 .input_notes(vec![(own_note_with_sender(faucet_id), None)])
1330 .build()
1331 .unwrap();
1332
1333 validate_output_note_senders(&request, account_id).unwrap();
1334 }
1335}