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 mut new_tags: Vec<NoteTagRecord> = note_updates
479 .updated_input_notes()
480 .filter_map(|note| {
481 let note = note.inner();
482
483 if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) =
484 note.state()
485 {
486 Some(NoteTagRecord::with_note_source(*tag, note.details_commitment()))
487 } else {
488 None
489 }
490 })
491 .collect();
492
493 new_tags.extend(note_updates.updated_output_notes().map(|note| {
494 let note = note.inner();
495 NoteTagRecord::with_note_source(note.metadata().tag(), note.details_commitment())
496 }));
497
498 Ok(TransactionStoreUpdate::new(
499 tx_result.executed_transaction().clone(),
500 submission_height,
501 note_updates,
502 tx_result.future_notes().to_vec(),
503 new_tags,
504 ))
505 }
506
507 pub async fn apply_transaction(
510 &self,
511 tx_result: &TransactionResult,
512 submission_height: BlockNumber,
513 ) -> Result<(), ClientError> {
514 let tx_update = self.get_transaction_store_update(tx_result, submission_height).await?;
515
516 self.apply_transaction_update(tx_update).await?;
517
518 for observer in &self.transaction_observers {
520 if let Err(err) = observer.apply(tx_result).await {
521 tracing::warn!(
522 observer = observer.name(),
523 error = ?err,
524 "TransactionObserver::apply failed; continuing with remaining observers",
525 );
526 }
527 }
528
529 Ok(())
530 }
531
532 pub async fn apply_transaction_update(
533 &self,
534 tx_update: TransactionStoreUpdate,
535 ) -> Result<(), ClientError> {
536 info!("Applying transaction to the local store...");
539
540 let executed_transaction = tx_update.executed_transaction();
541 let account_id = executed_transaction.account_id();
542
543 if self.account_reader(account_id).status().await?.is_locked() {
544 return Err(ClientError::AccountLocked(account_id));
545 }
546
547 self.store.apply_transaction(tx_update).await?;
548 info!("Transaction stored.");
549 Ok(())
550 }
551
552 pub async fn execute_program(
557 &mut self,
558 account_id: AccountId,
559 tx_script: TransactionScript,
560 advice_inputs: AdviceInputs,
561 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
562 ) -> Result<[Felt; MIN_STACK_DEPTH], ClientError> {
563 let (data_store, block_ref) =
564 self.prepare_program_execution(account_id, foreign_accounts).await?;
565
566 Ok(self
567 .build_executor(&data_store)?
568 .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
569 .await?)
570 }
571
572 #[cfg(feature = "dap")]
575 pub async fn execute_program_with_dap(
576 &mut self,
577 account_id: AccountId,
578 tx_script: TransactionScript,
579 advice_inputs: AdviceInputs,
580 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
581 ) -> Result<[Felt; MIN_STACK_DEPTH], ClientError> {
582 let (data_store, block_ref) =
583 self.prepare_program_execution(account_id, foreign_accounts).await?;
584
585 Ok(self
586 .build_dap_executor(&data_store)?
587 .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
588 .await?)
589 }
590
591 pub async fn validate_request(
601 &self,
602 account_id: AccountId,
603 transaction_request: &TransactionRequest,
604 ) -> Result<(), ClientError> {
605 self.validate_recency().await?;
606 validate_output_note_senders(transaction_request, account_id)?;
607 let account = self.try_get_account(account_id).await?;
608 validate_account_request(transaction_request, &account)
609 }
610
611 async fn validate_recency(&self) -> Result<(), ClientError> {
612 if let Some(max_block_number_delta) = self.max_block_number_delta {
613 let current_chain_tip =
614 self.rpc_api.get_block_header_by_number(None, false).await?.0.block_num();
615
616 if current_chain_tip > self.store.get_sync_height().await? + max_block_number_delta {
617 return Err(ClientError::RecencyConditionError(
618 "The client is too far behind the chain tip to execute the transaction",
619 ));
620 }
621 }
622 Ok(())
623 }
624
625 pub async fn ensure_ntx_scripts_registered(
638 &mut self,
639 account_id: AccountId,
640 scripts: &[NoteScript],
641 tx_prover: Arc<dyn TransactionProver>,
642 ) -> Result<(), ClientError> {
643 let mut missing_scripts = Vec::new();
644
645 for script in scripts {
646 if StandardNote::from_script(script).is_some() {
648 continue;
649 }
650
651 let script_root = script.root();
652
653 match self.rpc_api.get_note_script_by_root(script_root.into()).await {
655 Ok(Some(_)) => {},
656 Ok(None) => missing_scripts.push(script.clone()),
657 Err(source) => {
658 return Err(ClientError::NtxScriptRegistrationFailed {
659 script_root: script_root.into(),
660 source,
661 });
662 },
663 }
664 }
665
666 if missing_scripts.is_empty() {
667 return Ok(());
668 }
669
670 let registration_request = TransactionRequestBuilder::new().build_register_note_scripts(
671 account_id,
672 missing_scripts,
673 self.rng(),
674 )?;
675
676 let tx_result = self.execute_transaction(account_id, registration_request).await?;
677 let proven = self.prove_transaction_with(&tx_result, tx_prover).await?;
678 let submission_height = self.submit_proven_transaction(proven, &tx_result).await?;
679 self.apply_transaction(&tx_result, submission_height).await?;
680
681 Ok(())
682 }
683
684 pub(crate) async fn get_valid_input_notes(
690 &self,
691 account: &Account,
692 mut input_notes: InputNotes<InputNote>,
693 tx_args: TransactionArgs,
694 output_recipients: &[NoteRecipient],
695 ) -> Result<InputNotes<InputNote>, ClientError> {
696 loop {
697 let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
698 data_store.register_note_scripts(output_recipients.iter().map(|r| r.script().clone()));
699
700 data_store.mast_store().load_account_code(account.code());
701 let execution = NoteConsumptionChecker::new(&self.build_executor(&data_store)?)
702 .check_notes_consumability(
703 account.id(),
704 self.store.get_sync_height().await?,
705 input_notes.iter().map(|n| n.clone().into_note()).collect(),
706 tx_args.clone(),
707 )
708 .await?;
709
710 if execution.failed().is_empty() {
711 break;
712 }
713
714 let failed_note_ids: BTreeSet<NoteId> =
715 execution.failed().iter().map(|n| n.note().id()).collect();
716 let filtered_input_notes = InputNotes::new(
717 input_notes
718 .into_iter()
719 .filter(|note| !failed_note_ids.contains(¬e.id()))
720 .collect(),
721 )
722 .expect("Created from a valid input notes list");
723
724 input_notes = filtered_input_notes;
725 }
726
727 Ok(input_notes)
728 }
729
730 async fn retrieve_foreign_account_inputs(
737 &self,
738 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
739 ) -> Result<(Option<BlockNumber>, Vec<AccountInputs>), ClientError> {
740 if foreign_accounts.is_empty() {
741 return Ok((None, Vec::new()));
742 }
743
744 let block_num = self.store.get_sync_height().await?;
745 let mut return_foreign_account_inputs = Vec::with_capacity(foreign_accounts.len());
746
747 for foreign_account in foreign_accounts.into_values() {
748 let foreign_account_inputs = match foreign_account {
749 ForeignAccount::Public(account_id, storage_requirements) => {
750 fetch_public_account_inputs(
751 &self.store,
752 &self.rpc_api,
753 account_id,
754 storage_requirements,
755 AccountStateAt::Block(block_num),
756 )
757 .await?
758 },
759 ForeignAccount::Private(partial_account) => {
760 let account_id = partial_account.id();
761 let (_, account_proof) = self
762 .rpc_api
763 .get_account(
764 account_id,
765 GetAccountRequest::new().at(AccountStateAt::Block(block_num)),
766 )
767 .await?;
768 let (witness, _) = account_proof.into_parts();
769 AccountInputs::new(partial_account, witness)
770 },
771 };
772
773 return_foreign_account_inputs.push(foreign_account_inputs);
774 }
775
776 Ok((Some(block_num), return_foreign_account_inputs))
777 }
778
779 async fn prepare_program_execution(
783 &mut self,
784 account_id: AccountId,
785 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
786 ) -> Result<(ClientDataStore, BlockNumber), ClientError> {
787 let (fpi_block_number, foreign_account_inputs) =
788 self.retrieve_foreign_account_inputs(foreign_accounts).await?;
789
790 let block_ref = if let Some(block_number) = fpi_block_number {
791 block_number
792 } else {
793 self.get_sync_height().await?
794 };
795
796 let account_record = self
797 .store
798 .get_account(account_id)
799 .await?
800 .ok_or(ClientError::AccountDataNotFound(account_id))?;
801
802 let account: Account = account_record.try_into()?;
803
804 let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
805
806 data_store.mast_store().load_account_code(account.code());
808
809 for fpi_account in &foreign_account_inputs {
810 data_store.mast_store().load_account_code(fpi_account.code());
811 }
812
813 data_store.register_foreign_account_inputs(foreign_account_inputs);
814
815 Ok((data_store, block_ref))
816 }
817
818 pub(crate) fn build_executor<'store, 'auth, STORE: DataStore + Sync>(
821 &'auth self,
822 data_store: &'store STORE,
823 ) -> Result<TransactionExecutor<'store, 'auth, STORE, AUTH>, TransactionExecutorError> {
824 let mut executor = TransactionExecutor::new(data_store)
825 .with_options(self.exec_options)?
826 .with_source_manager(self.source_manager.clone());
827 if let Some(authenticator) = self.authenticator.as_deref() {
828 executor = executor.with_authenticator(authenticator);
829 }
830 Ok(executor)
831 }
832
833 async fn get_native_account_record(
836 &self,
837 account_id: AccountId,
838 ) -> Result<AccountRecord, ClientError> {
839 let account_record = self
840 .store
841 .get_account(account_id)
842 .await?
843 .ok_or(ClientError::AccountDataNotFound(account_id))?;
844 if account_record.is_watched() {
845 return Err(ClientError::AccountIsWatched(account_id));
846 }
847 Ok(account_record)
848 }
849
850 #[cfg(feature = "dap")]
852 pub(crate) fn build_dap_executor<'store, 'auth, STORE: DataStore + Sync>(
853 &'auth self,
854 data_store: &'store STORE,
855 ) -> Result<
856 TransactionExecutor<'store, 'auth, STORE, AUTH, dap_executor::DapProgramExecutor>,
857 TransactionExecutorError,
858 > {
859 Ok(self
860 .build_executor(data_store)?
861 .with_program_executor::<dap_executor::DapProgramExecutor>())
862 }
863
864 pub(crate) async fn get_account_interface(
866 &self,
867 account_id: AccountId,
868 ) -> Result<AccountInterface, ClientError> {
869 let account = self.try_get_account(account_id).await?;
870 Ok(AccountInterface::from_account(&account))
871 }
872
873 async fn get_note_updates(
876 &self,
877 submission_height: BlockNumber,
878 tx_result: &TransactionResult,
879 ) -> Result<NoteUpdateTracker, TransactionStoreUpdateError> {
880 let executed_tx = tx_result.executed_transaction();
881 let current_timestamp = self.store.get_current_timestamp();
882 let current_block_num = self.store.get_sync_height().await?;
883
884 let new_output_notes = executed_tx
886 .output_notes()
887 .iter()
888 .cloned()
889 .filter_map(|output_note| {
890 OutputNoteRecord::try_from_output_note(output_note, submission_height).ok()
891 })
892 .collect::<Vec<_>>();
893
894 let mut new_input_notes = vec![];
896 let output_notes: Vec<Note> =
897 notes_from_output(executed_tx.output_notes()).cloned().collect();
898 let note_screener = self.note_screener().clone();
899 let output_note_relevances = note_screener.can_consume_batch(&output_notes).await?;
900
901 for note in output_notes {
902 if output_note_relevances.contains_key(¬e.id()) {
903 let metadata = *note.metadata();
904 let tag = metadata.tag();
905 let attachments = note.attachments().clone();
906
907 new_input_notes.push(InputNoteRecord::new(
908 note.into(),
909 attachments,
910 current_timestamp,
911 ExpectedNoteState {
912 metadata: Some(metadata),
913 after_block_num: submission_height,
914 tag: Some(tag),
915 }
916 .into(),
917 ));
918 }
919 }
920
921 new_input_notes.extend(tx_result.future_notes().iter().map(|(note_details, tag)| {
923 InputNoteRecord::new(
924 note_details.clone(),
925 NoteAttachments::empty(),
926 None,
927 ExpectedNoteState {
928 metadata: None,
929 after_block_num: current_block_num,
930 tag: Some(*tag),
931 }
932 .into(),
933 )
934 }));
935
936 let consumed_note_ids =
941 executed_tx.tx_inputs().input_notes().iter().map(InputNote::id).collect();
942
943 let consumed_notes =
944 self.store.get_input_notes(NoteFilter::List(consumed_note_ids)).await?;
945
946 let tracked_note_ids =
947 consumed_notes.iter().filter_map(InputNoteRecord::id).collect::<BTreeSet<_>>();
948
949 for input_note in executed_tx.tx_inputs().input_notes() {
950 if !tracked_note_ids.contains(&input_note.id()) {
951 let mut input_note_record = InputNoteRecord::from(input_note.clone());
952 input_note_record.consumed_locally(
953 executed_tx.account_id(),
954 executed_tx.id(),
955 current_timestamp,
956 )?;
957 new_input_notes.push(input_note_record);
958 }
959 }
960
961 let mut updated_input_notes = vec![];
962
963 for mut input_note_record in consumed_notes {
964 if input_note_record.consumed_locally(
965 executed_tx.account_id(),
966 executed_tx.id(),
967 current_timestamp,
968 )? {
969 updated_input_notes.push(input_note_record);
970 }
971 }
972
973 Ok(NoteUpdateTracker::for_transaction_updates(
974 new_input_notes,
975 updated_input_notes,
976 new_output_notes,
977 ))
978 }
979}
980
981#[derive(Debug, thiserror::Error)]
987pub enum TransactionStoreUpdateError {
988 #[error("store error")]
989 Store(#[from] StoreError),
990 #[error("note screener error")]
991 NoteScreener(#[from] NoteScreenerError),
992 #[error("note record error")]
993 NoteRecord(#[from] NoteRecordError),
994}
995
996pub(crate) struct PreparedTransaction {
1001 pub(crate) notes: InputNotes<InputNote>,
1002 pub(crate) output_recipients: Vec<NoteRecipient>,
1003 pub(crate) future_notes: Vec<(NoteDetails, NoteTag)>,
1004 pub(crate) tx_args: TransactionArgs,
1005 pub(crate) foreign_account_inputs: Vec<AccountInputs>,
1006 pub(crate) block_num: BlockNumber,
1007 pub(crate) ignore_invalid_notes: bool,
1008}
1009
1010impl PreparedTransaction {
1011 pub(crate) fn output_note_scripts(&self) -> impl Iterator<Item = NoteScript> + '_ {
1014 self.output_recipients.iter().map(|recipient| recipient.script().clone())
1015 }
1016}
1017
1018fn get_outgoing_assets(
1023 transaction_request: &TransactionRequest,
1024) -> (BTreeMap<AccountId, u64>, Vec<NonFungibleAsset>) {
1025 let mut own_notes_assets = match transaction_request.script_template() {
1027 Some(TransactionScriptTemplate::SendNotes(notes)) => notes
1028 .iter()
1029 .map(|note| (note.id(), note.assets().clone()))
1030 .collect::<BTreeMap<_, _>>(),
1031 _ => BTreeMap::default(),
1032 };
1033 let mut output_notes_assets = transaction_request
1035 .expected_output_own_notes()
1036 .into_iter()
1037 .map(|note| (note.id(), note.assets().clone()))
1038 .collect::<BTreeMap<_, _>>();
1039
1040 output_notes_assets.append(&mut own_notes_assets);
1042
1043 let outgoing_assets = output_notes_assets.values().flat_map(|note_assets| note_assets.iter());
1045
1046 request::collect_assets(outgoing_assets)
1047}
1048
1049pub(super) fn validate_account_request(
1053 transaction_request: &TransactionRequest,
1054 account: &Account,
1055) -> Result<(), ClientError> {
1056 let account_interface = AccountInterface::from_account(account);
1057 if account_interface
1058 .components()
1059 .contains(&AccountComponentInterface::FungibleFaucet)
1060 {
1061 Ok(())
1063 } else {
1064 validate_basic_account_request(transaction_request, account)
1065 }
1066}
1067
1068fn validate_output_note_senders(
1076 transaction_request: &TransactionRequest,
1077 account_id: AccountId,
1078) -> Result<(), ClientError> {
1079 for note in transaction_request.expected_output_own_notes() {
1080 let sender = note.metadata().sender();
1081 if sender != account_id {
1082 return Err(ClientError::TransactionRequestError(
1083 TransactionRequestError::OutputNoteSenderMismatch {
1084 expected: account_id,
1085 actual: sender,
1086 },
1087 ));
1088 }
1089 }
1090
1091 Ok(())
1092}
1093
1094fn validate_basic_account_request(
1097 transaction_request: &TransactionRequest,
1098 account: &Account,
1099) -> Result<(), ClientError> {
1100 let (fungible_balance_map, non_fungible_set) = get_outgoing_assets(transaction_request);
1102
1103 let (incoming_fungible_balance_map, incoming_non_fungible_balance_set) =
1105 transaction_request.incoming_assets();
1106
1107 let mut available_fungible: BTreeMap<AccountId, u64> = BTreeMap::new();
1110 for asset in account.vault().assets() {
1111 if let Asset::Fungible(fungible) = asset {
1112 let balance = available_fungible.entry(fungible.faucet_id()).or_default();
1113 *balance = balance.saturating_add(fungible.amount().as_u64());
1114 }
1115 }
1116
1117 for (faucet_id, amount) in fungible_balance_map {
1120 let account_asset_amount = available_fungible.get(&faucet_id).copied().unwrap_or(0);
1121 let incoming_balance = incoming_fungible_balance_map.get(&faucet_id).unwrap_or(&0);
1122 if account_asset_amount + incoming_balance < amount {
1123 return Err(ClientError::AssetError(AssetError::FungibleAssetAmountNotSufficient {
1124 minuend: account_asset_amount,
1125 subtrahend: amount,
1126 }));
1127 }
1128 }
1129
1130 for non_fungible in &non_fungible_set {
1133 match account.vault().has_non_fungible_asset(*non_fungible) {
1134 Ok(true) => (),
1135 Ok(false) => {
1136 if !incoming_non_fungible_balance_set.contains(non_fungible) {
1138 return Err(ClientError::TransactionRequestError(
1139 TransactionRequestError::MissingNonFungibleAsset(non_fungible.faucet_id()),
1140 ));
1141 }
1142 },
1143 _ => {
1144 return Err(ClientError::TransactionRequestError(
1145 TransactionRequestError::MissingNonFungibleAsset(non_fungible.faucet_id()),
1146 ));
1147 },
1148 }
1149 }
1150
1151 Ok(())
1152}
1153
1154pub(crate) async fn fetch_public_account_inputs(
1161 store: &Arc<dyn Store>,
1162 rpc_api: &Arc<dyn NodeRpcClient>,
1163 account_id: AccountId,
1164 storage_requirements: AccountStorageRequirements,
1165 account_state_at: AccountStateAt,
1166) -> Result<AccountInputs, ClientError> {
1167 let known_code: Option<AccountCode> =
1168 store.get_foreign_account_code(vec![account_id]).await?.into_values().next();
1169
1170 let vault = store
1171 .get_account_header(account_id)
1172 .await?
1173 .map_or(VaultFetch::Always, |(header, ..)| {
1174 VaultFetch::IfChangedFrom(header.vault_root())
1175 });
1176
1177 let (block_num, mut account_proof) = rpc_api
1178 .get_account(
1179 account_id,
1180 GetAccountRequest::new()
1181 .with_storage(StorageMapFetch::Slots(storage_requirements.clone()))
1182 .at(account_state_at)
1183 .with_known_code(known_code)
1184 .with_vault(vault),
1185 )
1186 .await?;
1187
1188 if let Some(details) = account_proof.details_mut() {
1189 rpc_api.resolve_oversize_vault(account_id, block_num, details).await?;
1190 rpc_api.resolve_oversize_storage_maps(account_id, block_num, details).await?;
1191 }
1192
1193 let account_inputs = request::account_proof_into_inputs(account_proof, &storage_requirements)?;
1194
1195 let _ = store
1196 .upsert_foreign_account_code(account_id, account_inputs.code().clone())
1197 .await
1198 .inspect_err(|err| {
1199 tracing::warn!(
1200 %account_id,
1201 %err,
1202 "Failed to persist foreign account code to store"
1203 );
1204 });
1205
1206 Ok(account_inputs)
1207}
1208
1209pub fn notes_from_output(output_notes: &RawOutputNotes) -> impl Iterator<Item = &Note> {
1214 output_notes.iter().filter_map(|n| match n {
1215 RawOutputNote::Full(n) => Some(n),
1216 RawOutputNote::Partial(_) => None,
1217 })
1218}
1219
1220pub(crate) fn validate_executed_transaction(
1223 executed_transaction: &ExecutedTransaction,
1224 expected_output_recipients: &[NoteRecipient],
1225) -> Result<(), ClientError> {
1226 let tx_output_recipient_digests = executed_transaction
1227 .output_notes()
1228 .iter()
1229 .filter_map(|n| n.recipient().map(NoteRecipient::digest))
1230 .collect::<Vec<_>>();
1231
1232 let missing_recipient_digest: Vec<Word> = expected_output_recipients
1233 .iter()
1234 .filter_map(|recipient| {
1235 (!tx_output_recipient_digests.contains(&recipient.digest()))
1236 .then_some(recipient.digest())
1237 })
1238 .collect();
1239
1240 if !missing_recipient_digest.is_empty() {
1241 return Err(ClientError::MissingOutputRecipients(missing_recipient_digest));
1242 }
1243
1244 Ok(())
1245}
1246
1247#[cfg(test)]
1251mod tests {
1252 use alloc::vec;
1253
1254 use miden_protocol::Word;
1255 use miden_protocol::account::AccountId;
1256 use miden_protocol::asset::FungibleAsset;
1257 use miden_protocol::crypto::rand::RandomCoin;
1258 use miden_protocol::note::{Note, NoteAttachments, NoteType};
1259 use miden_protocol::testing::account_id::{
1260 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
1261 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
1262 ACCOUNT_ID_SENDER,
1263 };
1264 use miden_standards::note::P2idNote;
1265
1266 use super::{TransactionRequestBuilder, validate_output_note_senders};
1267 use crate::ClientError;
1268 use crate::transaction::TransactionRequestError;
1269
1270 fn own_note_with_sender(sender: AccountId) -> Note {
1271 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
1272 let target_id =
1273 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1274 let mut rng = RandomCoin::new(Word::default());
1275
1276 P2idNote::create(
1277 sender,
1278 target_id,
1279 vec![FungibleAsset::new(faucet_id, 100).unwrap().into()],
1280 NoteType::Public,
1281 NoteAttachments::empty(),
1282 &mut rng,
1283 )
1284 .unwrap()
1285 }
1286
1287 #[test]
1288 fn output_note_with_foreign_sender_is_rejected() {
1289 let account_id =
1290 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1291 let foreign_sender = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
1292 assert_ne!(account_id, foreign_sender);
1293
1294 let request = TransactionRequestBuilder::new()
1295 .own_output_notes(vec![own_note_with_sender(foreign_sender)])
1296 .build()
1297 .unwrap();
1298
1299 let err = validate_output_note_senders(&request, account_id).unwrap_err();
1300 match err {
1301 ClientError::TransactionRequestError(
1302 TransactionRequestError::OutputNoteSenderMismatch { expected, actual },
1303 ) => {
1304 assert_eq!(expected, account_id);
1305 assert_eq!(actual, foreign_sender);
1306 },
1307 other => panic!("expected OutputNoteSenderMismatch, got {other:?}"),
1308 }
1309 }
1310
1311 #[test]
1312 fn output_note_with_matching_sender_is_accepted() {
1313 let account_id =
1314 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1315
1316 let request = TransactionRequestBuilder::new()
1317 .own_output_notes(vec![own_note_with_sender(account_id)])
1318 .build()
1319 .unwrap();
1320
1321 validate_output_note_senders(&request, account_id).unwrap();
1322 }
1323
1324 #[test]
1325 fn request_without_own_output_notes_is_accepted() {
1326 let account_id =
1327 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1328 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
1329
1330 let request = TransactionRequestBuilder::new()
1332 .input_notes(vec![(own_note_with_sender(faucet_id), None)])
1333 .build()
1334 .unwrap();
1335
1336 validate_output_note_senders(&request, account_id).unwrap();
1337 }
1338}