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 result;
150pub use miden_protocol::transaction::{
153 ExecutedTransaction,
154 InputNote,
155 InputNotes,
156 OutputNote,
157 OutputNotes,
158 ProvenTransaction,
159 PublicOutputNote,
160 RawOutputNote,
161 RawOutputNotes,
162 TransactionArgs,
163 TransactionId,
164 TransactionInputs,
165 TransactionKernel,
166 TransactionScript,
167 TransactionScriptRoot,
168 TransactionSummary,
169};
170pub use miden_protocol::vm::{AdviceInputs, AdviceMap};
171pub use miden_standards::account::interface::{AccountComponentInterface, AccountInterface};
172pub use miden_tx::auth::TransactionAuthenticator;
173pub use miden_tx::{
174 DataStoreError,
175 LocalTransactionProver,
176 ProvingOptions,
177 TransactionExecutorError,
178 TransactionProverError,
179};
180pub use result::TransactionResult;
181
182impl<AUTH> Client<AUTH>
184where
185 AUTH: TransactionAuthenticator + Sync + 'static,
186{
187 pub async fn get_transactions(
192 &self,
193 filter: TransactionFilter,
194 ) -> Result<Vec<TransactionRecord>, ClientError> {
195 self.store.get_transactions(filter).await.map_err(Into::into)
196 }
197
198 pub fn new_transaction_batch(&self) -> BatchBuilder<'_, AUTH> {
206 let inner_data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
207 BatchBuilder {
208 client: self,
209 data_store: InMemoryBatchDataStore::new(inner_data_store),
210 pushed_txs: Vec::new(),
211 consumed_input_notes: BTreeSet::new(),
212 }
213 }
214
215 pub async fn submit_new_transaction(
224 &mut self,
225 account_id: AccountId,
226 transaction_request: TransactionRequest,
227 ) -> Result<TransactionId, ClientError> {
228 let prover = self.tx_prover.clone();
229 self.submit_new_transaction_with_prover(account_id, transaction_request, prover)
230 .await
231 }
232
233 pub async fn submit_new_transaction_with_prover(
240 &mut self,
241 account_id: AccountId,
242 transaction_request: TransactionRequest,
243 tx_prover: Arc<dyn TransactionProver>,
244 ) -> Result<TransactionId, ClientError> {
245 if !transaction_request.expected_ntx_scripts().is_empty() {
248 Box::pin(self.ensure_ntx_scripts_registered(
249 account_id,
250 transaction_request.expected_ntx_scripts(),
251 tx_prover.clone(),
252 ))
253 .await?;
254 }
255
256 let tx_result = self.execute_transaction(account_id, transaction_request).await?;
257 let tx_id = tx_result.executed_transaction().id();
258
259 let proven_transaction = self.prove_transaction_with(&tx_result, tx_prover).await?;
260 let submission_height =
261 self.submit_proven_transaction(proven_transaction, &tx_result).await?;
262
263 let tx_update =
272 Box::new(self.get_transaction_store_update(&tx_result, submission_height).await?);
273
274 if let Err(apply_err) = self.apply_transaction_update((*tx_update).clone()).await {
275 info!(
276 "apply_transaction_update failed for submitted tx {tx_id}; returning \
277 ApplyTransactionAfterSubmitFailed with the pending update attached: {apply_err}"
278 );
279 return Err(ClientError::ApplyTransactionAfterSubmitFailed {
280 pending_update: tx_update,
281 source: Box::new(apply_err),
282 });
283 }
284
285 Ok(tx_id)
286 }
287
288 pub async fn execute_transaction(
298 &mut self,
299 account_id: AccountId,
300 transaction_request: TransactionRequest,
301 ) -> Result<TransactionResult, ClientError> {
302 let account: Account = self.get_native_account_record(account_id).await?.try_into()?;
303
304 let prep = self.prepare_transaction(&account, transaction_request).await?;
305
306 let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
307 data_store.register_note_scripts(prep.output_note_scripts());
308 for fpi_account in &prep.foreign_account_inputs {
309 data_store.mast_store().load_account_code(fpi_account.code());
310 }
311 data_store.register_foreign_account_inputs(prep.foreign_account_inputs);
312
313 data_store.mast_store().load_account_code(account.code());
314
315 let mut notes = prep.notes;
316 if prep.ignore_invalid_notes {
317 notes = self
318 .get_valid_input_notes(
319 &account,
320 notes,
321 prep.tx_args.clone(),
322 &prep.output_recipients,
323 )
324 .await?;
325 }
326
327 let executed_transaction = self
328 .build_executor(&data_store)?
329 .execute_transaction(account_id, prep.block_num, notes, prep.tx_args)
330 .await?;
331
332 validate_executed_transaction(&executed_transaction, &prep.output_recipients)?;
333 TransactionResult::new(executed_transaction, prep.future_notes)
334 }
335
336 pub(crate) async fn prepare_transaction(
348 &self,
349 account: &Account,
350 transaction_request: TransactionRequest,
351 ) -> Result<PreparedTransaction, ClientError> {
352 let account_id = account.id();
353 self.validate_recency().await?;
354 validate_account_request(&transaction_request, account)?;
355
356 let mut stored_note_records = self
358 .store
359 .get_input_notes(NoteFilter::List(transaction_request.input_note_ids().collect()))
360 .await?;
361
362 for note in &stored_note_records {
364 if note.is_consumed() {
365 let id = note.id().expect(
366 "stored note records reaching this check carry metadata so id() is Some",
367 );
368 return Err(ClientError::TransactionRequestError(
369 TransactionRequestError::InputNoteAlreadyConsumed(id),
370 ));
371 }
372 }
373
374 stored_note_records.retain(InputNoteRecord::is_authenticated);
376
377 let notes = transaction_request.build_input_notes(stored_note_records)?;
378
379 let output_recipients =
380 transaction_request.expected_output_recipients().cloned().collect::<Vec<_>>();
381
382 let future_notes: Vec<(NoteDetails, NoteTag)> =
383 transaction_request.expected_future_notes().cloned().collect();
384
385 let tx_script = transaction_request
386 .build_transaction_script(&self.get_account_interface(account_id).await?)?;
387
388 let foreign_accounts = transaction_request.foreign_accounts().clone();
389
390 let (fpi_block_num, foreign_account_inputs) =
391 self.retrieve_foreign_account_inputs(foreign_accounts).await?;
392
393 let ignore_invalid_notes = transaction_request.ignore_invalid_input_notes();
394
395 let block_num = if let Some(block_num) = fpi_block_num {
396 block_num
397 } else {
398 self.store.get_sync_height().await?
399 };
400
401 let tx_args = transaction_request.into_transaction_args(tx_script);
402
403 Ok(PreparedTransaction {
404 notes,
405 output_recipients,
406 future_notes,
407 tx_args,
408 foreign_account_inputs,
409 block_num,
410 ignore_invalid_notes,
411 })
412 }
413
414 pub async fn prove_transaction(
416 &self,
417 tx_result: &TransactionResult,
418 ) -> Result<ProvenTransaction, ClientError> {
419 self.prove_transaction_with(tx_result, self.tx_prover.clone()).await
420 }
421
422 pub async fn prove_transaction_with(
424 &self,
425 tx_result: &TransactionResult,
426 tx_prover: Arc<dyn TransactionProver>,
427 ) -> Result<ProvenTransaction, ClientError> {
428 info!("Proving transaction...");
429
430 let proven_transaction =
431 tx_prover.prove(tx_result.executed_transaction().clone().into()).await?;
432
433 info!("Transaction proven.");
434
435 Ok(proven_transaction)
436 }
437
438 pub async fn submit_proven_transaction(
441 &mut self,
442 proven_transaction: ProvenTransaction,
443 transaction_inputs: impl Into<TransactionInputs>,
444 ) -> Result<BlockNumber, ClientError> {
445 info!("Submitting transaction to the network...");
446 let block_num = self
447 .rpc_api
448 .submit_proven_transaction(proven_transaction, transaction_inputs.into())
449 .await?;
450 info!("Transaction submitted.");
451
452 Ok(block_num)
453 }
454
455 pub async fn get_transaction_store_update(
458 &self,
459 tx_result: &TransactionResult,
460 submission_height: BlockNumber,
461 ) -> Result<TransactionStoreUpdate, TransactionStoreUpdateError> {
462 let note_updates = self.get_note_updates(submission_height, tx_result).await?;
463
464 let mut new_tags: Vec<NoteTagRecord> = note_updates
465 .updated_input_notes()
466 .filter_map(|note| {
467 let note = note.inner();
468
469 if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) =
470 note.state()
471 {
472 Some(NoteTagRecord::with_note_source(*tag, note.details_commitment()))
473 } else {
474 None
475 }
476 })
477 .collect();
478
479 new_tags.extend(note_updates.updated_output_notes().map(|note| {
480 let note = note.inner();
481 NoteTagRecord::with_note_source(note.metadata().tag(), note.details_commitment())
482 }));
483
484 Ok(TransactionStoreUpdate::new(
485 tx_result.executed_transaction().clone(),
486 submission_height,
487 note_updates,
488 tx_result.future_notes().to_vec(),
489 new_tags,
490 ))
491 }
492
493 pub async fn apply_transaction(
496 &self,
497 tx_result: &TransactionResult,
498 submission_height: BlockNumber,
499 ) -> Result<(), ClientError> {
500 let tx_update = self.get_transaction_store_update(tx_result, submission_height).await?;
501
502 self.apply_transaction_update(tx_update).await
503 }
504
505 pub async fn apply_transaction_update(
506 &self,
507 tx_update: TransactionStoreUpdate,
508 ) -> Result<(), ClientError> {
509 info!("Applying transaction to the local store...");
512
513 let executed_transaction = tx_update.executed_transaction();
514 let account_id = executed_transaction.account_id();
515
516 if self.account_reader(account_id).status().await?.is_locked() {
517 return Err(ClientError::AccountLocked(account_id));
518 }
519
520 self.store.apply_transaction(tx_update).await?;
521 info!("Transaction stored.");
522 Ok(())
523 }
524
525 pub async fn execute_program(
530 &mut self,
531 account_id: AccountId,
532 tx_script: TransactionScript,
533 advice_inputs: AdviceInputs,
534 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
535 ) -> Result<[Felt; MIN_STACK_DEPTH], ClientError> {
536 let (data_store, block_ref) =
537 self.prepare_program_execution(account_id, foreign_accounts).await?;
538
539 Ok(self
540 .build_executor(&data_store)?
541 .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
542 .await?)
543 }
544
545 #[cfg(feature = "dap")]
548 pub async fn execute_program_with_dap(
549 &mut self,
550 account_id: AccountId,
551 tx_script: TransactionScript,
552 advice_inputs: AdviceInputs,
553 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
554 ) -> Result<[Felt; MIN_STACK_DEPTH], ClientError> {
555 let (data_store, block_ref) =
556 self.prepare_program_execution(account_id, foreign_accounts).await?;
557
558 Ok(self
559 .build_dap_executor(&data_store)?
560 .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
561 .await?)
562 }
563
564 pub async fn validate_request(
574 &self,
575 account_id: AccountId,
576 transaction_request: &TransactionRequest,
577 ) -> Result<(), ClientError> {
578 self.validate_recency().await?;
579 validate_output_note_senders(transaction_request, account_id)?;
580 let account = self.try_get_account(account_id).await?;
581 validate_account_request(transaction_request, &account)
582 }
583
584 async fn validate_recency(&self) -> Result<(), ClientError> {
585 if let Some(max_block_number_delta) = self.max_block_number_delta {
586 let current_chain_tip =
587 self.rpc_api.get_block_header_by_number(None, false).await?.0.block_num();
588
589 if current_chain_tip > self.store.get_sync_height().await? + max_block_number_delta {
590 return Err(ClientError::RecencyConditionError(
591 "The client is too far behind the chain tip to execute the transaction",
592 ));
593 }
594 }
595 Ok(())
596 }
597
598 pub async fn ensure_ntx_scripts_registered(
611 &mut self,
612 account_id: AccountId,
613 scripts: &[NoteScript],
614 tx_prover: Arc<dyn TransactionProver>,
615 ) -> Result<(), ClientError> {
616 let mut missing_scripts = Vec::new();
617
618 for script in scripts {
619 if StandardNote::from_script(script).is_some() {
621 continue;
622 }
623
624 let script_root = script.root();
625
626 match self.rpc_api.get_note_script_by_root(script_root.into()).await {
628 Ok(Some(_)) => {},
629 Ok(None) => missing_scripts.push(script.clone()),
630 Err(source) => {
631 return Err(ClientError::NtxScriptRegistrationFailed {
632 script_root: script_root.into(),
633 source,
634 });
635 },
636 }
637 }
638
639 if missing_scripts.is_empty() {
640 return Ok(());
641 }
642
643 let registration_request = TransactionRequestBuilder::new().build_register_note_scripts(
644 account_id,
645 missing_scripts,
646 self.rng(),
647 )?;
648
649 let tx_result = self.execute_transaction(account_id, registration_request).await?;
650 let proven = self.prove_transaction_with(&tx_result, tx_prover).await?;
651 let submission_height = self.submit_proven_transaction(proven, &tx_result).await?;
652 self.apply_transaction(&tx_result, submission_height).await?;
653
654 Ok(())
655 }
656
657 pub(crate) async fn get_valid_input_notes(
663 &self,
664 account: &Account,
665 mut input_notes: InputNotes<InputNote>,
666 tx_args: TransactionArgs,
667 output_recipients: &[NoteRecipient],
668 ) -> Result<InputNotes<InputNote>, ClientError> {
669 loop {
670 let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
671 data_store.register_note_scripts(output_recipients.iter().map(|r| r.script().clone()));
672
673 data_store.mast_store().load_account_code(account.code());
674 let execution = NoteConsumptionChecker::new(&self.build_executor(&data_store)?)
675 .check_notes_consumability(
676 account.id(),
677 self.store.get_sync_height().await?,
678 input_notes.iter().map(|n| n.clone().into_note()).collect(),
679 tx_args.clone(),
680 )
681 .await?;
682
683 if execution.failed().is_empty() {
684 break;
685 }
686
687 let failed_note_ids: BTreeSet<NoteId> =
688 execution.failed().iter().map(|n| n.note().id()).collect();
689 let filtered_input_notes = InputNotes::new(
690 input_notes
691 .into_iter()
692 .filter(|note| !failed_note_ids.contains(¬e.id()))
693 .collect(),
694 )
695 .expect("Created from a valid input notes list");
696
697 input_notes = filtered_input_notes;
698 }
699
700 Ok(input_notes)
701 }
702
703 async fn retrieve_foreign_account_inputs(
710 &self,
711 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
712 ) -> Result<(Option<BlockNumber>, Vec<AccountInputs>), ClientError> {
713 if foreign_accounts.is_empty() {
714 return Ok((None, Vec::new()));
715 }
716
717 let block_num = self.store.get_sync_height().await?;
718 let mut return_foreign_account_inputs = Vec::with_capacity(foreign_accounts.len());
719
720 for foreign_account in foreign_accounts.into_values() {
721 let foreign_account_inputs = match foreign_account {
722 ForeignAccount::Public(account_id, storage_requirements) => {
723 fetch_public_account_inputs(
724 &self.store,
725 &self.rpc_api,
726 account_id,
727 storage_requirements,
728 AccountStateAt::Block(block_num),
729 )
730 .await?
731 },
732 ForeignAccount::Private(partial_account) => {
733 let account_id = partial_account.id();
734 let (_, account_proof) = self
735 .rpc_api
736 .get_account(
737 account_id,
738 GetAccountRequest::new().at(AccountStateAt::Block(block_num)),
739 )
740 .await?;
741 let (witness, _) = account_proof.into_parts();
742 AccountInputs::new(partial_account, witness)
743 },
744 };
745
746 return_foreign_account_inputs.push(foreign_account_inputs);
747 }
748
749 Ok((Some(block_num), return_foreign_account_inputs))
750 }
751
752 async fn prepare_program_execution(
756 &mut self,
757 account_id: AccountId,
758 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
759 ) -> Result<(ClientDataStore, BlockNumber), ClientError> {
760 let (fpi_block_number, foreign_account_inputs) =
761 self.retrieve_foreign_account_inputs(foreign_accounts).await?;
762
763 let block_ref = if let Some(block_number) = fpi_block_number {
764 block_number
765 } else {
766 self.get_sync_height().await?
767 };
768
769 let account_record = self
770 .store
771 .get_account(account_id)
772 .await?
773 .ok_or(ClientError::AccountDataNotFound(account_id))?;
774
775 let account: Account = account_record.try_into()?;
776
777 let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
778
779 data_store.mast_store().load_account_code(account.code());
781
782 for fpi_account in &foreign_account_inputs {
783 data_store.mast_store().load_account_code(fpi_account.code());
784 }
785
786 data_store.register_foreign_account_inputs(foreign_account_inputs);
787
788 Ok((data_store, block_ref))
789 }
790
791 pub(crate) fn build_executor<'store, 'auth, STORE: DataStore + Sync>(
794 &'auth self,
795 data_store: &'store STORE,
796 ) -> Result<TransactionExecutor<'store, 'auth, STORE, AUTH>, TransactionExecutorError> {
797 let mut executor = TransactionExecutor::new(data_store)
798 .with_options(self.exec_options)?
799 .with_source_manager(self.source_manager.clone());
800 if let Some(authenticator) = self.authenticator.as_deref() {
801 executor = executor.with_authenticator(authenticator);
802 }
803 Ok(executor)
804 }
805
806 async fn get_native_account_record(
809 &self,
810 account_id: AccountId,
811 ) -> Result<AccountRecord, ClientError> {
812 let account_record = self
813 .store
814 .get_account(account_id)
815 .await?
816 .ok_or(ClientError::AccountDataNotFound(account_id))?;
817 if account_record.is_watched() {
818 return Err(ClientError::AccountIsWatched(account_id));
819 }
820 Ok(account_record)
821 }
822
823 #[cfg(feature = "dap")]
825 pub(crate) fn build_dap_executor<'store, 'auth, STORE: DataStore + Sync>(
826 &'auth self,
827 data_store: &'store STORE,
828 ) -> Result<
829 TransactionExecutor<'store, 'auth, STORE, AUTH, dap_executor::DapProgramExecutor>,
830 TransactionExecutorError,
831 > {
832 Ok(self
833 .build_executor(data_store)?
834 .with_program_executor::<dap_executor::DapProgramExecutor>())
835 }
836
837 pub(crate) async fn get_account_interface(
839 &self,
840 account_id: AccountId,
841 ) -> Result<AccountInterface, ClientError> {
842 let account = self.try_get_account(account_id).await?;
843 Ok(AccountInterface::from_account(&account))
844 }
845
846 async fn get_note_updates(
849 &self,
850 submission_height: BlockNumber,
851 tx_result: &TransactionResult,
852 ) -> Result<NoteUpdateTracker, TransactionStoreUpdateError> {
853 let executed_tx = tx_result.executed_transaction();
854 let current_timestamp = self.store.get_current_timestamp();
855 let current_block_num = self.store.get_sync_height().await?;
856
857 let new_output_notes = executed_tx
859 .output_notes()
860 .iter()
861 .cloned()
862 .filter_map(|output_note| {
863 OutputNoteRecord::try_from_output_note(output_note, submission_height).ok()
864 })
865 .collect::<Vec<_>>();
866
867 let mut new_input_notes = vec![];
869 let output_notes: Vec<Note> =
870 notes_from_output(executed_tx.output_notes()).cloned().collect();
871 let note_screener = self.note_screener().clone();
872 let output_note_relevances = note_screener.can_consume_batch(&output_notes).await?;
873
874 for note in output_notes {
875 if output_note_relevances.contains_key(¬e.id()) {
876 let metadata = *note.metadata();
877 let tag = metadata.tag();
878 let attachments = note.attachments().clone();
879
880 new_input_notes.push(InputNoteRecord::new(
881 note.into(),
882 attachments,
883 current_timestamp,
884 ExpectedNoteState {
885 metadata: Some(metadata),
886 after_block_num: submission_height,
887 tag: Some(tag),
888 }
889 .into(),
890 ));
891 }
892 }
893
894 new_input_notes.extend(tx_result.future_notes().iter().map(|(note_details, tag)| {
896 InputNoteRecord::new(
897 note_details.clone(),
898 NoteAttachments::empty(),
899 None,
900 ExpectedNoteState {
901 metadata: None,
902 after_block_num: current_block_num,
903 tag: Some(*tag),
904 }
905 .into(),
906 )
907 }));
908
909 let consumed_note_ids =
914 executed_tx.tx_inputs().input_notes().iter().map(InputNote::id).collect();
915
916 let consumed_notes =
917 self.store.get_input_notes(NoteFilter::List(consumed_note_ids)).await?;
918
919 let tracked_note_ids =
920 consumed_notes.iter().filter_map(InputNoteRecord::id).collect::<BTreeSet<_>>();
921
922 for input_note in executed_tx.tx_inputs().input_notes() {
923 if !tracked_note_ids.contains(&input_note.id()) {
924 let mut input_note_record = InputNoteRecord::from(input_note.clone());
925 input_note_record.consumed_locally(
926 executed_tx.account_id(),
927 executed_tx.id(),
928 current_timestamp,
929 )?;
930 new_input_notes.push(input_note_record);
931 }
932 }
933
934 let mut updated_input_notes = vec![];
935
936 for mut input_note_record in consumed_notes {
937 if input_note_record.consumed_locally(
938 executed_tx.account_id(),
939 executed_tx.id(),
940 current_timestamp,
941 )? {
942 updated_input_notes.push(input_note_record);
943 }
944 }
945
946 Ok(NoteUpdateTracker::for_transaction_updates(
947 new_input_notes,
948 updated_input_notes,
949 new_output_notes,
950 ))
951 }
952}
953
954#[derive(Debug, thiserror::Error)]
960pub enum TransactionStoreUpdateError {
961 #[error("store error")]
962 Store(#[from] StoreError),
963 #[error("note screener error")]
964 NoteScreener(#[from] NoteScreenerError),
965 #[error("note record error")]
966 NoteRecord(#[from] NoteRecordError),
967}
968
969pub(crate) struct PreparedTransaction {
974 pub(crate) notes: InputNotes<InputNote>,
975 pub(crate) output_recipients: Vec<NoteRecipient>,
976 pub(crate) future_notes: Vec<(NoteDetails, NoteTag)>,
977 pub(crate) tx_args: TransactionArgs,
978 pub(crate) foreign_account_inputs: Vec<AccountInputs>,
979 pub(crate) block_num: BlockNumber,
980 pub(crate) ignore_invalid_notes: bool,
981}
982
983impl PreparedTransaction {
984 pub(crate) fn output_note_scripts(&self) -> impl Iterator<Item = NoteScript> + '_ {
987 self.output_recipients.iter().map(|recipient| recipient.script().clone())
988 }
989}
990
991fn get_outgoing_assets(
996 transaction_request: &TransactionRequest,
997) -> (BTreeMap<AccountId, u64>, Vec<NonFungibleAsset>) {
998 let mut own_notes_assets = match transaction_request.script_template() {
1000 Some(TransactionScriptTemplate::SendNotes(notes)) => notes
1001 .iter()
1002 .map(|note| (note.id(), note.assets().clone()))
1003 .collect::<BTreeMap<_, _>>(),
1004 _ => BTreeMap::default(),
1005 };
1006 let mut output_notes_assets = transaction_request
1008 .expected_output_own_notes()
1009 .into_iter()
1010 .map(|note| (note.id(), note.assets().clone()))
1011 .collect::<BTreeMap<_, _>>();
1012
1013 output_notes_assets.append(&mut own_notes_assets);
1015
1016 let outgoing_assets = output_notes_assets.values().flat_map(|note_assets| note_assets.iter());
1018
1019 request::collect_assets(outgoing_assets)
1020}
1021
1022pub(super) fn validate_account_request(
1026 transaction_request: &TransactionRequest,
1027 account: &Account,
1028) -> Result<(), ClientError> {
1029 let account_interface = AccountInterface::from_account(account);
1030 if account_interface
1031 .components()
1032 .contains(&AccountComponentInterface::FungibleFaucet)
1033 {
1034 Ok(())
1036 } else {
1037 validate_basic_account_request(transaction_request, account)
1038 }
1039}
1040
1041fn validate_output_note_senders(
1049 transaction_request: &TransactionRequest,
1050 account_id: AccountId,
1051) -> Result<(), ClientError> {
1052 for note in transaction_request.expected_output_own_notes() {
1053 let sender = note.metadata().sender();
1054 if sender != account_id {
1055 return Err(ClientError::TransactionRequestError(
1056 TransactionRequestError::OutputNoteSenderMismatch {
1057 expected: account_id,
1058 actual: sender,
1059 },
1060 ));
1061 }
1062 }
1063
1064 Ok(())
1065}
1066
1067fn validate_basic_account_request(
1070 transaction_request: &TransactionRequest,
1071 account: &Account,
1072) -> Result<(), ClientError> {
1073 let (fungible_balance_map, non_fungible_set) = get_outgoing_assets(transaction_request);
1075
1076 let (incoming_fungible_balance_map, incoming_non_fungible_balance_set) =
1078 transaction_request.incoming_assets();
1079
1080 let mut available_fungible: BTreeMap<AccountId, u64> = BTreeMap::new();
1083 for asset in account.vault().assets() {
1084 if let Asset::Fungible(fungible) = asset {
1085 let balance = available_fungible.entry(fungible.faucet_id()).or_default();
1086 *balance = balance.saturating_add(fungible.amount().as_u64());
1087 }
1088 }
1089
1090 for (faucet_id, amount) in fungible_balance_map {
1093 let account_asset_amount = available_fungible.get(&faucet_id).copied().unwrap_or(0);
1094 let incoming_balance = incoming_fungible_balance_map.get(&faucet_id).unwrap_or(&0);
1095 if account_asset_amount + incoming_balance < amount {
1096 return Err(ClientError::AssetError(AssetError::FungibleAssetAmountNotSufficient {
1097 minuend: account_asset_amount,
1098 subtrahend: amount,
1099 }));
1100 }
1101 }
1102
1103 for non_fungible in &non_fungible_set {
1106 match account.vault().has_non_fungible_asset(*non_fungible) {
1107 Ok(true) => (),
1108 Ok(false) => {
1109 if !incoming_non_fungible_balance_set.contains(non_fungible) {
1111 return Err(ClientError::TransactionRequestError(
1112 TransactionRequestError::MissingNonFungibleAsset(non_fungible.faucet_id()),
1113 ));
1114 }
1115 },
1116 _ => {
1117 return Err(ClientError::TransactionRequestError(
1118 TransactionRequestError::MissingNonFungibleAsset(non_fungible.faucet_id()),
1119 ));
1120 },
1121 }
1122 }
1123
1124 Ok(())
1125}
1126
1127pub(crate) async fn fetch_public_account_inputs(
1134 store: &Arc<dyn Store>,
1135 rpc_api: &Arc<dyn NodeRpcClient>,
1136 account_id: AccountId,
1137 storage_requirements: AccountStorageRequirements,
1138 account_state_at: AccountStateAt,
1139) -> Result<AccountInputs, ClientError> {
1140 let known_code: Option<AccountCode> =
1141 store.get_foreign_account_code(vec![account_id]).await?.into_values().next();
1142
1143 let vault = store
1144 .get_account_header(account_id)
1145 .await?
1146 .map_or(VaultFetch::Always, |(header, ..)| {
1147 VaultFetch::IfChangedFrom(header.vault_root())
1148 });
1149
1150 let (block_num, mut account_proof) = rpc_api
1151 .get_account(
1152 account_id,
1153 GetAccountRequest::new()
1154 .with_storage(StorageMapFetch::Slots(storage_requirements.clone()))
1155 .at(account_state_at)
1156 .with_known_code(known_code)
1157 .with_vault(vault),
1158 )
1159 .await?;
1160
1161 if let Some(details) = account_proof.details_mut() {
1162 rpc_api.resolve_oversize_vault(account_id, block_num, details).await?;
1163 rpc_api.resolve_oversize_storage_maps(account_id, block_num, details).await?;
1164 }
1165
1166 let account_inputs = request::account_proof_into_inputs(account_proof, &storage_requirements)?;
1167
1168 let _ = store
1169 .upsert_foreign_account_code(account_id, account_inputs.code().clone())
1170 .await
1171 .inspect_err(|err| {
1172 tracing::warn!(
1173 %account_id,
1174 %err,
1175 "Failed to persist foreign account code to store"
1176 );
1177 });
1178
1179 Ok(account_inputs)
1180}
1181
1182pub fn notes_from_output(output_notes: &RawOutputNotes) -> impl Iterator<Item = &Note> {
1187 output_notes.iter().filter_map(|n| match n {
1188 RawOutputNote::Full(n) => Some(n),
1189 RawOutputNote::Partial(_) => None,
1190 })
1191}
1192
1193pub(crate) fn validate_executed_transaction(
1196 executed_transaction: &ExecutedTransaction,
1197 expected_output_recipients: &[NoteRecipient],
1198) -> Result<(), ClientError> {
1199 let tx_output_recipient_digests = executed_transaction
1200 .output_notes()
1201 .iter()
1202 .filter_map(|n| n.recipient().map(NoteRecipient::digest))
1203 .collect::<Vec<_>>();
1204
1205 let missing_recipient_digest: Vec<Word> = expected_output_recipients
1206 .iter()
1207 .filter_map(|recipient| {
1208 (!tx_output_recipient_digests.contains(&recipient.digest()))
1209 .then_some(recipient.digest())
1210 })
1211 .collect();
1212
1213 if !missing_recipient_digest.is_empty() {
1214 return Err(ClientError::MissingOutputRecipients(missing_recipient_digest));
1215 }
1216
1217 Ok(())
1218}
1219
1220#[cfg(test)]
1224mod tests {
1225 use alloc::vec;
1226
1227 use miden_protocol::Word;
1228 use miden_protocol::account::AccountId;
1229 use miden_protocol::asset::FungibleAsset;
1230 use miden_protocol::crypto::rand::RandomCoin;
1231 use miden_protocol::note::{Note, NoteAttachments, NoteType};
1232 use miden_protocol::testing::account_id::{
1233 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
1234 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
1235 ACCOUNT_ID_SENDER,
1236 };
1237 use miden_standards::note::P2idNote;
1238
1239 use super::{TransactionRequestBuilder, validate_output_note_senders};
1240 use crate::ClientError;
1241 use crate::transaction::TransactionRequestError;
1242
1243 fn own_note_with_sender(sender: AccountId) -> Note {
1244 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
1245 let target_id =
1246 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1247 let mut rng = RandomCoin::new(Word::default());
1248
1249 P2idNote::create(
1250 sender,
1251 target_id,
1252 vec![FungibleAsset::new(faucet_id, 100).unwrap().into()],
1253 NoteType::Public,
1254 NoteAttachments::empty(),
1255 &mut rng,
1256 )
1257 .unwrap()
1258 }
1259
1260 #[test]
1261 fn output_note_with_foreign_sender_is_rejected() {
1262 let account_id =
1263 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1264 let foreign_sender = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
1265 assert_ne!(account_id, foreign_sender);
1266
1267 let request = TransactionRequestBuilder::new()
1268 .own_output_notes(vec![own_note_with_sender(foreign_sender)])
1269 .build()
1270 .unwrap();
1271
1272 let err = validate_output_note_senders(&request, account_id).unwrap_err();
1273 match err {
1274 ClientError::TransactionRequestError(
1275 TransactionRequestError::OutputNoteSenderMismatch { expected, actual },
1276 ) => {
1277 assert_eq!(expected, account_id);
1278 assert_eq!(actual, foreign_sender);
1279 },
1280 other => panic!("expected OutputNoteSenderMismatch, got {other:?}"),
1281 }
1282 }
1283
1284 #[test]
1285 fn output_note_with_matching_sender_is_accepted() {
1286 let account_id =
1287 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1288
1289 let request = TransactionRequestBuilder::new()
1290 .own_output_notes(vec![own_note_with_sender(account_id)])
1291 .build()
1292 .unwrap();
1293
1294 validate_output_note_senders(&request, account_id).unwrap();
1295 }
1296
1297 #[test]
1298 fn request_without_own_output_notes_is_accepted() {
1299 let account_id =
1300 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1301 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
1302
1303 let request = TransactionRequestBuilder::new()
1305 .input_notes(vec![(own_note_with_sender(faucet_id), None)])
1306 .build()
1307 .unwrap();
1308
1309 validate_output_note_senders(&request, account_id).unwrap();
1310 }
1311}