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 return Err(ClientError::TransactionRequestError(
380 TransactionRequestError::InputNoteAlreadyConsumed(note.details_commitment()),
381 ));
382 }
383 }
384
385 stored_note_records.retain(InputNoteRecord::is_authenticated);
387
388 let notes = transaction_request.build_input_notes(stored_note_records)?;
389
390 let output_recipients =
391 transaction_request.expected_output_recipients().cloned().collect::<Vec<_>>();
392
393 let future_notes: Vec<(NoteDetails, NoteTag)> =
394 transaction_request.expected_future_notes().cloned().collect();
395
396 let tx_script = transaction_request
397 .build_transaction_script(&self.get_account_interface(account_id).await?)?;
398
399 let foreign_accounts = transaction_request.foreign_accounts().clone();
400
401 let (fpi_block_num, foreign_account_inputs) =
402 self.retrieve_foreign_account_inputs(foreign_accounts).await?;
403
404 let ignore_invalid_notes = transaction_request.ignore_invalid_input_notes();
405
406 let block_num = if let Some(block_num) = fpi_block_num {
407 block_num
408 } else {
409 self.store.get_sync_height().await?
410 };
411
412 let tx_args = transaction_request.into_transaction_args(tx_script);
413
414 Ok(PreparedTransaction {
415 notes,
416 output_recipients,
417 future_notes,
418 tx_args,
419 foreign_account_inputs,
420 block_num,
421 ignore_invalid_notes,
422 })
423 }
424
425 pub async fn prove_transaction(
427 &self,
428 tx_result: &TransactionResult,
429 ) -> Result<ProvenTransaction, ClientError> {
430 self.prove_transaction_with(tx_result, self.tx_prover.clone()).await
431 }
432
433 pub async fn prove_transaction_with(
435 &self,
436 tx_result: &TransactionResult,
437 tx_prover: Arc<dyn TransactionProver>,
438 ) -> Result<ProvenTransaction, ClientError> {
439 info!("Proving transaction...");
440
441 let proven_transaction =
442 tx_prover.prove(tx_result.executed_transaction().clone().into()).await?;
443
444 info!("Transaction proven.");
445
446 Ok(proven_transaction)
447 }
448
449 pub async fn submit_proven_transaction(
452 &mut self,
453 proven_transaction: ProvenTransaction,
454 transaction_inputs: impl Into<TransactionInputs>,
455 ) -> Result<BlockNumber, ClientError> {
456 info!("Submitting transaction to the network...");
457 let block_num = self
458 .rpc_api
459 .submit_proven_transaction(proven_transaction, transaction_inputs.into())
460 .await?;
461 info!("Transaction submitted.");
462
463 Ok(block_num)
464 }
465
466 pub async fn get_transaction_store_update(
469 &self,
470 tx_result: &TransactionResult,
471 submission_height: BlockNumber,
472 ) -> Result<TransactionStoreUpdate, TransactionStoreUpdateError> {
473 let note_updates = self.get_note_updates(submission_height, tx_result).await?;
474
475 let new_tags: Vec<NoteTagRecord> = note_updates
478 .updated_input_notes()
479 .filter_map(|note| {
480 let note = note.inner();
481
482 if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) =
483 note.state()
484 {
485 Some(NoteTagRecord::with_note_source(*tag, note.details_commitment()))
486 } else {
487 None
488 }
489 })
490 .collect();
491
492 Ok(TransactionStoreUpdate::new(
493 tx_result.executed_transaction().clone(),
494 submission_height,
495 note_updates,
496 tx_result.future_notes().to_vec(),
497 new_tags,
498 ))
499 }
500
501 pub async fn apply_transaction(
504 &self,
505 tx_result: &TransactionResult,
506 submission_height: BlockNumber,
507 ) -> Result<(), ClientError> {
508 let tx_update = self.get_transaction_store_update(tx_result, submission_height).await?;
509
510 self.apply_transaction_update(tx_update).await?;
511
512 for observer in &self.transaction_observers {
514 if let Err(err) = observer.apply(tx_result).await {
515 tracing::warn!(
516 observer = observer.name(),
517 error = ?err,
518 "TransactionObserver::apply failed; continuing with remaining observers",
519 );
520 }
521 }
522
523 Ok(())
524 }
525
526 pub async fn apply_transaction_update(
527 &self,
528 tx_update: TransactionStoreUpdate,
529 ) -> Result<(), ClientError> {
530 info!("Applying transaction to the local store...");
533
534 let executed_transaction = tx_update.executed_transaction();
535 let account_id = executed_transaction.account_id();
536
537 if self.account_reader(account_id).status().await?.is_locked() {
538 return Err(ClientError::AccountLocked(account_id));
539 }
540
541 self.store.apply_transaction(tx_update).await?;
542 info!("Transaction stored.");
543 Ok(())
544 }
545
546 pub async fn execute_program(
551 &mut self,
552 account_id: AccountId,
553 tx_script: TransactionScript,
554 advice_inputs: AdviceInputs,
555 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
556 ) -> Result<[Felt; MIN_STACK_DEPTH], ClientError> {
557 let (data_store, block_ref) =
558 self.prepare_program_execution(account_id, foreign_accounts).await?;
559
560 Ok(self
561 .build_executor(&data_store)?
562 .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
563 .await?)
564 }
565
566 #[cfg(feature = "dap")]
569 pub async fn execute_program_with_dap(
570 &mut self,
571 account_id: AccountId,
572 tx_script: TransactionScript,
573 advice_inputs: AdviceInputs,
574 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
575 ) -> Result<[Felt; MIN_STACK_DEPTH], ClientError> {
576 let (data_store, block_ref) =
577 self.prepare_program_execution(account_id, foreign_accounts).await?;
578
579 Ok(self
580 .build_dap_executor(&data_store)?
581 .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
582 .await?)
583 }
584
585 pub async fn validate_request(
595 &self,
596 account_id: AccountId,
597 transaction_request: &TransactionRequest,
598 ) -> Result<(), ClientError> {
599 self.validate_recency().await?;
600 validate_output_note_senders(transaction_request, account_id)?;
601 let account = self.try_get_account(account_id).await?;
602 validate_account_request(transaction_request, &account)
603 }
604
605 async fn validate_recency(&self) -> Result<(), ClientError> {
606 if let Some(max_block_number_delta) = self.max_block_number_delta {
607 let current_chain_tip =
608 self.rpc_api.get_block_header_by_number(None, false).await?.0.block_num();
609
610 if current_chain_tip > self.store.get_sync_height().await? + max_block_number_delta {
611 return Err(ClientError::RecencyConditionError(
612 "The client is too far behind the chain tip to execute the transaction",
613 ));
614 }
615 }
616 Ok(())
617 }
618
619 pub async fn ensure_ntx_scripts_registered(
632 &mut self,
633 account_id: AccountId,
634 scripts: &[NoteScript],
635 tx_prover: Arc<dyn TransactionProver>,
636 ) -> Result<(), ClientError> {
637 let mut missing_scripts = Vec::new();
638
639 for script in scripts {
640 if StandardNote::from_script(script).is_some() {
642 continue;
643 }
644
645 let script_root = script.root();
646
647 match self.rpc_api.get_note_script_by_root(script_root.into()).await {
649 Ok(Some(_)) => {},
650 Ok(None) => missing_scripts.push(script.clone()),
651 Err(source) => {
652 return Err(ClientError::NtxScriptRegistrationFailed {
653 script_root: script_root.into(),
654 source,
655 });
656 },
657 }
658 }
659
660 if missing_scripts.is_empty() {
661 return Ok(());
662 }
663
664 let registration_request = TransactionRequestBuilder::new().build_register_note_scripts(
665 account_id,
666 missing_scripts,
667 self.rng(),
668 )?;
669
670 let tx_result = self.execute_transaction(account_id, registration_request).await?;
671 let proven = self.prove_transaction_with(&tx_result, tx_prover).await?;
672 let submission_height = self.submit_proven_transaction(proven, &tx_result).await?;
673 self.apply_transaction(&tx_result, submission_height).await?;
674
675 Ok(())
676 }
677
678 pub(crate) async fn get_valid_input_notes(
684 &self,
685 account: &Account,
686 mut input_notes: InputNotes<InputNote>,
687 tx_args: TransactionArgs,
688 output_recipients: &[NoteRecipient],
689 ) -> Result<InputNotes<InputNote>, ClientError> {
690 loop {
691 let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
692 data_store.register_note_scripts(output_recipients.iter().map(|r| r.script().clone()));
693
694 data_store.mast_store().load_account_code(account.code());
695 let execution = NoteConsumptionChecker::new(&self.build_executor(&data_store)?)
696 .check_notes_consumability(
697 account.id(),
698 self.store.get_sync_height().await?,
699 input_notes.iter().map(|n| n.clone().into_note()).collect(),
700 tx_args.clone(),
701 )
702 .await?;
703
704 if execution.failed().is_empty() {
705 break;
706 }
707
708 let failed_note_ids: BTreeSet<NoteId> =
709 execution.failed().iter().map(|n| n.note().id()).collect();
710 let filtered_input_notes = InputNotes::new(
711 input_notes
712 .into_iter()
713 .filter(|note| !failed_note_ids.contains(¬e.id()))
714 .collect(),
715 )
716 .expect("Created from a valid input notes list");
717
718 input_notes = filtered_input_notes;
719 }
720
721 Ok(input_notes)
722 }
723
724 async fn retrieve_foreign_account_inputs(
731 &self,
732 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
733 ) -> Result<(Option<BlockNumber>, Vec<AccountInputs>), ClientError> {
734 if foreign_accounts.is_empty() {
735 return Ok((None, Vec::new()));
736 }
737
738 let block_num = self.store.get_sync_height().await?;
739 let mut return_foreign_account_inputs = Vec::with_capacity(foreign_accounts.len());
740
741 for foreign_account in foreign_accounts.into_values() {
742 let foreign_account_inputs = match foreign_account {
743 ForeignAccount::Public(account_id, storage_requirements) => {
744 fetch_public_account_inputs(
745 &self.store,
746 &self.rpc_api,
747 account_id,
748 storage_requirements,
749 AccountStateAt::Block(block_num),
750 )
751 .await?
752 },
753 ForeignAccount::Private(partial_account) => {
754 let account_id = partial_account.id();
755 let (_, account_proof) = self
756 .rpc_api
757 .get_account(
758 account_id,
759 GetAccountRequest::new().at(AccountStateAt::Block(block_num)),
760 )
761 .await?;
762 let (witness, _) = account_proof.into_parts();
763 AccountInputs::new(partial_account, witness)
764 },
765 };
766
767 return_foreign_account_inputs.push(foreign_account_inputs);
768 }
769
770 Ok((Some(block_num), return_foreign_account_inputs))
771 }
772
773 async fn prepare_program_execution(
777 &mut self,
778 account_id: AccountId,
779 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
780 ) -> Result<(ClientDataStore, BlockNumber), ClientError> {
781 let (fpi_block_number, foreign_account_inputs) =
782 self.retrieve_foreign_account_inputs(foreign_accounts).await?;
783
784 let block_ref = if let Some(block_number) = fpi_block_number {
785 block_number
786 } else {
787 self.get_sync_height().await?
788 };
789
790 let account_record = self
791 .store
792 .get_account(account_id)
793 .await?
794 .ok_or(ClientError::AccountDataNotFound(account_id))?;
795
796 let account: Account = account_record.try_into()?;
797
798 let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
799
800 data_store.mast_store().load_account_code(account.code());
802
803 for fpi_account in &foreign_account_inputs {
804 data_store.mast_store().load_account_code(fpi_account.code());
805 }
806
807 data_store.register_foreign_account_inputs(foreign_account_inputs);
808
809 Ok((data_store, block_ref))
810 }
811
812 pub(crate) fn build_executor<'store, 'auth, STORE: DataStore + Sync>(
815 &'auth self,
816 data_store: &'store STORE,
817 ) -> Result<TransactionExecutor<'store, 'auth, STORE, AUTH>, TransactionExecutorError> {
818 let mut executor = TransactionExecutor::new(data_store)
819 .with_options(self.exec_options)?
820 .with_source_manager(self.source_manager.clone());
821 if let Some(authenticator) = self.authenticator.as_deref() {
822 executor = executor.with_authenticator(authenticator);
823 }
824 Ok(executor)
825 }
826
827 async fn get_native_account_record(
830 &self,
831 account_id: AccountId,
832 ) -> Result<AccountRecord, ClientError> {
833 let account_record = self
834 .store
835 .get_account(account_id)
836 .await?
837 .ok_or(ClientError::AccountDataNotFound(account_id))?;
838 if account_record.is_watched() {
839 return Err(ClientError::AccountIsWatched(account_id));
840 }
841 Ok(account_record)
842 }
843
844 #[cfg(feature = "dap")]
846 pub(crate) fn build_dap_executor<'store, 'auth, STORE: DataStore + Sync>(
847 &'auth self,
848 data_store: &'store STORE,
849 ) -> Result<
850 TransactionExecutor<'store, 'auth, STORE, AUTH, dap_executor::DapProgramExecutor>,
851 TransactionExecutorError,
852 > {
853 Ok(self
854 .build_executor(data_store)?
855 .with_program_executor::<dap_executor::DapProgramExecutor>())
856 }
857
858 pub(crate) async fn get_account_interface(
860 &self,
861 account_id: AccountId,
862 ) -> Result<AccountInterface, ClientError> {
863 let account = self.try_get_account(account_id).await?;
864 Ok(AccountInterface::from_account(&account))
865 }
866
867 async fn get_note_updates(
870 &self,
871 submission_height: BlockNumber,
872 tx_result: &TransactionResult,
873 ) -> Result<NoteUpdateTracker, TransactionStoreUpdateError> {
874 let executed_tx = tx_result.executed_transaction();
875 let current_timestamp = self.store.get_current_timestamp();
876 let current_block_num = self.store.get_sync_height().await?;
877
878 let new_output_notes = executed_tx
880 .output_notes()
881 .iter()
882 .cloned()
883 .filter_map(|output_note| {
884 OutputNoteRecord::try_from_output_note(output_note, submission_height).ok()
885 })
886 .collect::<Vec<_>>();
887
888 let mut new_input_notes = vec![];
890 let output_notes: Vec<Note> =
891 notes_from_output(executed_tx.output_notes()).cloned().collect();
892 let note_screener = self.note_screener().clone();
893 let output_note_relevances = note_screener.can_consume_batch(&output_notes).await?;
894
895 for note in output_notes {
896 if output_note_relevances.contains_key(¬e.id()) {
897 let metadata = *note.metadata();
898 let tag = metadata.tag();
899 let attachments = note.attachments().clone();
900
901 new_input_notes.push(InputNoteRecord::new(
902 note.into(),
903 attachments,
904 current_timestamp,
905 ExpectedNoteState {
906 metadata: Some(metadata),
907 after_block_num: submission_height,
908 tag: Some(tag),
909 }
910 .into(),
911 ));
912 }
913 }
914
915 new_input_notes.extend(tx_result.future_notes().iter().map(|(note_details, tag)| {
917 InputNoteRecord::new(
918 note_details.clone(),
919 NoteAttachments::empty(),
920 None,
921 ExpectedNoteState {
922 metadata: None,
923 after_block_num: current_block_num,
924 tag: Some(*tag),
925 }
926 .into(),
927 )
928 }));
929
930 let consumed_note_ids =
935 executed_tx.tx_inputs().input_notes().iter().map(InputNote::id).collect();
936
937 let consumed_notes =
938 self.store.get_input_notes(NoteFilter::List(consumed_note_ids)).await?;
939
940 let tracked_note_ids =
941 consumed_notes.iter().filter_map(InputNoteRecord::id).collect::<BTreeSet<_>>();
942
943 for input_note in executed_tx.tx_inputs().input_notes() {
944 if !tracked_note_ids.contains(&input_note.id()) {
945 let mut input_note_record = InputNoteRecord::from(input_note.clone());
946 input_note_record.consumed_locally(
947 executed_tx.account_id(),
948 executed_tx.id(),
949 current_timestamp,
950 )?;
951 new_input_notes.push(input_note_record);
952 }
953 }
954
955 let mut updated_input_notes = vec![];
956
957 for mut input_note_record in consumed_notes {
958 if input_note_record.consumed_locally(
959 executed_tx.account_id(),
960 executed_tx.id(),
961 current_timestamp,
962 )? {
963 updated_input_notes.push(input_note_record);
964 }
965 }
966
967 Ok(NoteUpdateTracker::for_transaction_updates(
968 new_input_notes,
969 updated_input_notes,
970 new_output_notes,
971 ))
972 }
973}
974
975#[derive(Debug, thiserror::Error)]
981pub enum TransactionStoreUpdateError {
982 #[error("store error")]
983 Store(#[from] StoreError),
984 #[error("note screener error")]
985 NoteScreener(#[from] NoteScreenerError),
986 #[error("note record error")]
987 NoteRecord(#[from] NoteRecordError),
988}
989
990pub(crate) struct PreparedTransaction {
995 pub(crate) notes: InputNotes<InputNote>,
996 pub(crate) output_recipients: Vec<NoteRecipient>,
997 pub(crate) future_notes: Vec<(NoteDetails, NoteTag)>,
998 pub(crate) tx_args: TransactionArgs,
999 pub(crate) foreign_account_inputs: Vec<AccountInputs>,
1000 pub(crate) block_num: BlockNumber,
1001 pub(crate) ignore_invalid_notes: bool,
1002}
1003
1004impl PreparedTransaction {
1005 pub(crate) fn output_note_scripts(&self) -> impl Iterator<Item = NoteScript> + '_ {
1008 self.output_recipients.iter().map(|recipient| recipient.script().clone())
1009 }
1010}
1011
1012fn get_outgoing_assets(
1017 transaction_request: &TransactionRequest,
1018) -> (BTreeMap<AccountId, u64>, Vec<NonFungibleAsset>) {
1019 let mut own_notes_assets = match transaction_request.script_template() {
1021 Some(TransactionScriptTemplate::SendNotes(notes)) => notes
1022 .iter()
1023 .map(|note| (note.id(), note.assets().clone()))
1024 .collect::<BTreeMap<_, _>>(),
1025 _ => BTreeMap::default(),
1026 };
1027 let mut output_notes_assets = transaction_request
1029 .expected_output_own_notes()
1030 .into_iter()
1031 .map(|note| (note.id(), note.assets().clone()))
1032 .collect::<BTreeMap<_, _>>();
1033
1034 output_notes_assets.append(&mut own_notes_assets);
1036
1037 let outgoing_assets = output_notes_assets.values().flat_map(|note_assets| note_assets.iter());
1039
1040 request::collect_assets(outgoing_assets)
1041}
1042
1043pub(super) fn validate_account_request(
1047 transaction_request: &TransactionRequest,
1048 account: &Account,
1049) -> Result<(), ClientError> {
1050 let account_interface = AccountInterface::from_account(account);
1051 if account_interface
1052 .components()
1053 .contains(&AccountComponentInterface::FungibleFaucet)
1054 {
1055 Ok(())
1057 } else {
1058 validate_basic_account_request(transaction_request, account)
1059 }
1060}
1061
1062fn validate_output_note_senders(
1070 transaction_request: &TransactionRequest,
1071 account_id: AccountId,
1072) -> Result<(), ClientError> {
1073 for note in transaction_request.expected_output_own_notes() {
1074 let sender = note.metadata().sender();
1075 if sender != account_id {
1076 return Err(ClientError::TransactionRequestError(
1077 TransactionRequestError::OutputNoteSenderMismatch {
1078 expected: account_id,
1079 actual: sender,
1080 },
1081 ));
1082 }
1083 }
1084
1085 Ok(())
1086}
1087
1088fn validate_basic_account_request(
1091 transaction_request: &TransactionRequest,
1092 account: &Account,
1093) -> Result<(), ClientError> {
1094 let (fungible_balance_map, non_fungible_set) = get_outgoing_assets(transaction_request);
1096
1097 let (incoming_fungible_balance_map, incoming_non_fungible_balance_set) =
1099 transaction_request.incoming_assets();
1100
1101 let mut available_fungible: BTreeMap<AccountId, u64> = BTreeMap::new();
1104 for asset in account.vault().assets() {
1105 if let Asset::Fungible(fungible) = asset {
1106 let balance = available_fungible.entry(fungible.faucet_id()).or_default();
1107 *balance = balance.saturating_add(fungible.amount().as_u64());
1108 }
1109 }
1110
1111 for (faucet_id, amount) in fungible_balance_map {
1114 let account_asset_amount = available_fungible.get(&faucet_id).copied().unwrap_or(0);
1115 let incoming_balance = incoming_fungible_balance_map.get(&faucet_id).unwrap_or(&0);
1116 if account_asset_amount + incoming_balance < amount {
1117 return Err(ClientError::AssetError(AssetError::FungibleAssetAmountNotSufficient {
1118 minuend: account_asset_amount,
1119 subtrahend: amount,
1120 }));
1121 }
1122 }
1123
1124 for non_fungible in &non_fungible_set {
1127 match account.vault().has_non_fungible_asset(*non_fungible) {
1128 Ok(true) => (),
1129 Ok(false) => {
1130 if !incoming_non_fungible_balance_set.contains(non_fungible) {
1132 return Err(ClientError::TransactionRequestError(
1133 TransactionRequestError::MissingNonFungibleAsset(non_fungible.faucet_id()),
1134 ));
1135 }
1136 },
1137 _ => {
1138 return Err(ClientError::TransactionRequestError(
1139 TransactionRequestError::MissingNonFungibleAsset(non_fungible.faucet_id()),
1140 ));
1141 },
1142 }
1143 }
1144
1145 Ok(())
1146}
1147
1148pub(crate) async fn fetch_public_account_inputs(
1155 store: &Arc<dyn Store>,
1156 rpc_api: &Arc<dyn NodeRpcClient>,
1157 account_id: AccountId,
1158 storage_requirements: AccountStorageRequirements,
1159 account_state_at: AccountStateAt,
1160) -> Result<AccountInputs, ClientError> {
1161 let known_code: Option<AccountCode> =
1162 store.get_foreign_account_code(vec![account_id]).await?.into_values().next();
1163
1164 let vault = store
1165 .get_account_header(account_id)
1166 .await?
1167 .map_or(VaultFetch::Always, |(header, ..)| {
1168 VaultFetch::IfChangedFrom(header.vault_root())
1169 });
1170
1171 let (block_num, mut account_proof) = rpc_api
1172 .get_account(
1173 account_id,
1174 GetAccountRequest::new()
1175 .with_storage(StorageMapFetch::Slots(storage_requirements.clone()))
1176 .at(account_state_at)
1177 .with_known_code(known_code)
1178 .with_vault(vault),
1179 )
1180 .await?;
1181
1182 if let Some(details) = account_proof.details_mut() {
1183 rpc_api.resolve_oversize_vault(account_id, block_num, details).await?;
1184 rpc_api.resolve_oversize_storage_maps(account_id, block_num, details).await?;
1185 }
1186
1187 let account_inputs = request::account_proof_into_inputs(account_proof, &storage_requirements)?;
1188
1189 let _ = store
1190 .upsert_foreign_account_code(account_id, account_inputs.code().clone())
1191 .await
1192 .inspect_err(|err| {
1193 tracing::warn!(
1194 %account_id,
1195 %err,
1196 "Failed to persist foreign account code to store"
1197 );
1198 });
1199
1200 Ok(account_inputs)
1201}
1202
1203pub fn notes_from_output(output_notes: &RawOutputNotes) -> impl Iterator<Item = &Note> {
1208 output_notes.iter().filter_map(|n| match n {
1209 RawOutputNote::Full(n) => Some(n),
1210 RawOutputNote::Partial(_) => None,
1211 })
1212}
1213
1214pub(crate) fn validate_executed_transaction(
1217 executed_transaction: &ExecutedTransaction,
1218 expected_output_recipients: &[NoteRecipient],
1219) -> Result<(), ClientError> {
1220 let tx_output_recipient_digests = executed_transaction
1221 .output_notes()
1222 .iter()
1223 .filter_map(|n| n.recipient().map(NoteRecipient::digest))
1224 .collect::<Vec<_>>();
1225
1226 let missing_recipient_digest: Vec<Word> = expected_output_recipients
1227 .iter()
1228 .filter_map(|recipient| {
1229 (!tx_output_recipient_digests.contains(&recipient.digest()))
1230 .then_some(recipient.digest())
1231 })
1232 .collect();
1233
1234 if !missing_recipient_digest.is_empty() {
1235 return Err(ClientError::MissingOutputRecipients(missing_recipient_digest));
1236 }
1237
1238 Ok(())
1239}
1240
1241#[cfg(test)]
1245mod tests {
1246 use alloc::vec;
1247
1248 use miden_protocol::Word;
1249 use miden_protocol::account::AccountId;
1250 use miden_protocol::asset::FungibleAsset;
1251 use miden_protocol::crypto::rand::RandomCoin;
1252 use miden_protocol::note::{Note, NoteAttachments, NoteType};
1253 use miden_protocol::testing::account_id::{
1254 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
1255 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
1256 ACCOUNT_ID_SENDER,
1257 };
1258 use miden_standards::note::P2idNote;
1259
1260 use super::{TransactionRequestBuilder, validate_output_note_senders};
1261 use crate::ClientError;
1262 use crate::transaction::TransactionRequestError;
1263
1264 fn own_note_with_sender(sender: AccountId) -> Note {
1265 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
1266 let target_id =
1267 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1268 let mut rng = RandomCoin::new(Word::default());
1269
1270 P2idNote::create(
1271 sender,
1272 target_id,
1273 vec![FungibleAsset::new(faucet_id, 100).unwrap().into()],
1274 NoteType::Public,
1275 NoteAttachments::empty(),
1276 &mut rng,
1277 )
1278 .unwrap()
1279 }
1280
1281 #[test]
1282 fn output_note_with_foreign_sender_is_rejected() {
1283 let account_id =
1284 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1285 let foreign_sender = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
1286 assert_ne!(account_id, foreign_sender);
1287
1288 let request = TransactionRequestBuilder::new()
1289 .own_output_notes(vec![own_note_with_sender(foreign_sender)])
1290 .build()
1291 .unwrap();
1292
1293 let err = validate_output_note_senders(&request, account_id).unwrap_err();
1294 match err {
1295 ClientError::TransactionRequestError(
1296 TransactionRequestError::OutputNoteSenderMismatch { expected, actual },
1297 ) => {
1298 assert_eq!(expected, account_id);
1299 assert_eq!(actual, foreign_sender);
1300 },
1301 other => panic!("expected OutputNoteSenderMismatch, got {other:?}"),
1302 }
1303 }
1304
1305 #[test]
1306 fn output_note_with_matching_sender_is_accepted() {
1307 let account_id =
1308 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1309
1310 let request = TransactionRequestBuilder::new()
1311 .own_output_notes(vec![own_note_with_sender(account_id)])
1312 .build()
1313 .unwrap();
1314
1315 validate_output_note_senders(&request, account_id).unwrap();
1316 }
1317
1318 #[test]
1319 fn request_without_own_output_notes_is_accepted() {
1320 let account_id =
1321 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1322 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
1323
1324 let request = TransactionRequestBuilder::new()
1326 .input_notes(vec![(own_note_with_sender(faucet_id), None)])
1327 .build()
1328 .unwrap();
1329
1330 validate_output_note_senders(&request, account_id).unwrap();
1331 }
1332}