1use alloc::collections::{BTreeMap, BTreeSet};
66use alloc::string::ToString;
67use alloc::sync::Arc;
68use alloc::vec::Vec;
69
70use miden_objects::account::{Account, AccountId};
71use miden_objects::asset::{Asset, NonFungibleAsset};
72use miden_objects::block::BlockNumber;
73use miden_objects::note::{Note, NoteDetails, NoteId, NoteRecipient, NoteScript, NoteTag};
74use miden_objects::transaction::AccountInputs;
75use miden_objects::{AssetError, Felt, Word};
76use miden_tx::{DataStore, NoteConsumptionChecker, TransactionExecutor};
77use tracing::info;
78
79use super::Client;
80use crate::ClientError;
81use crate::note::{NoteScreener, NoteUpdateTracker};
82use crate::rpc::domain::account::AccountProof;
83use crate::store::data_store::ClientDataStore;
84use crate::store::input_note_states::ExpectedNoteState;
85use crate::store::{
86 InputNoteRecord,
87 InputNoteState,
88 NoteFilter,
89 OutputNoteRecord,
90 StoreError,
91 TransactionFilter,
92};
93use crate::sync::NoteTagRecord;
94
95mod prover;
96pub use prover::TransactionProver;
97
98mod record;
99pub use record::{
100 DiscardCause,
101 TransactionDetails,
102 TransactionRecord,
103 TransactionStatus,
104 TransactionStatusVariant,
105};
106
107mod store_update;
108pub use store_update::TransactionStoreUpdate;
109
110mod request;
111pub use request::{
112 ForeignAccount,
113 NoteArgs,
114 PaymentNoteDescription,
115 SwapTransactionData,
116 TransactionRequest,
117 TransactionRequestBuilder,
118 TransactionRequestError,
119 TransactionScriptTemplate,
120};
121
122mod result;
123pub use miden_lib::account::interface::{AccountComponentInterface, AccountInterface};
126pub use miden_lib::transaction::TransactionKernel;
127pub use miden_objects::transaction::{
128 ExecutedTransaction,
129 InputNote,
130 InputNotes,
131 OutputNote,
132 OutputNotes,
133 ProvenTransaction,
134 TransactionArgs,
135 TransactionId,
136 TransactionInputs,
137 TransactionScript,
138 TransactionSummary,
139};
140pub use miden_objects::vm::{AdviceInputs, AdviceMap};
141pub use miden_tx::auth::TransactionAuthenticator;
142pub use miden_tx::{
143 DataStoreError,
144 LocalTransactionProver,
145 ProvingOptions,
146 TransactionExecutorError,
147 TransactionProverError,
148};
149pub use result::TransactionResult;
150
151impl<AUTH> Client<AUTH>
153where
154 AUTH: TransactionAuthenticator + Sync + 'static,
155{
156 pub async fn get_transactions(
161 &self,
162 filter: TransactionFilter,
163 ) -> Result<Vec<TransactionRecord>, ClientError> {
164 self.store.get_transactions(filter).await.map_err(Into::into)
165 }
166
167 pub async fn submit_new_transaction(
177 &mut self,
178 account_id: AccountId,
179 transaction_request: TransactionRequest,
180 ) -> Result<TransactionId, ClientError> {
181 let tx_result = self.execute_transaction(account_id, transaction_request).await?;
182 let tx_id = tx_result.executed_transaction().id();
183
184 let proven_transaction = self.prove_transaction(&tx_result).await?;
185 let submission_height =
186 self.submit_proven_transaction(proven_transaction, &tx_result).await?;
187
188 self.apply_transaction(&tx_result, submission_height).await?;
189
190 Ok(tx_id)
191 }
192
193 pub async fn execute_transaction(
207 &mut self,
208 account_id: AccountId,
209 transaction_request: TransactionRequest,
210 ) -> Result<TransactionResult, ClientError> {
211 self.validate_request(account_id, &transaction_request).await?;
213
214 let authenticated_input_note_ids: Vec<NoteId> =
217 transaction_request.authenticated_input_note_ids().collect::<Vec<_>>();
218
219 let authenticated_note_records = self
220 .store
221 .get_input_notes(NoteFilter::List(authenticated_input_note_ids))
222 .await?;
223
224 let unauthenticated_input_notes = transaction_request
226 .unauthenticated_input_notes()
227 .iter()
228 .cloned()
229 .map(Into::into)
230 .collect::<Vec<_>>();
231
232 self.store.upsert_input_notes(&unauthenticated_input_notes).await?;
233
234 let mut notes = transaction_request.build_input_notes(authenticated_note_records)?;
235
236 let output_recipients =
237 transaction_request.expected_output_recipients().cloned().collect::<Vec<_>>();
238
239 let future_notes: Vec<(NoteDetails, NoteTag)> =
240 transaction_request.expected_future_notes().cloned().collect();
241
242 let tx_script = transaction_request.build_transaction_script(
243 &self.get_account_interface(account_id).await?,
244 self.in_debug_mode().into(),
245 )?;
246
247 let foreign_accounts = transaction_request.foreign_accounts().clone();
248
249 let (fpi_block_num, foreign_account_inputs) =
251 self.retrieve_foreign_account_inputs(foreign_accounts).await?;
252
253 let ignore_invalid_notes = transaction_request.ignore_invalid_input_notes();
254
255 let data_store = ClientDataStore::new(self.store.clone());
256 data_store.register_foreign_account_inputs(foreign_account_inputs.iter().cloned());
257 for fpi_account in &foreign_account_inputs {
258 data_store.mast_store().load_account_code(fpi_account.code());
259 }
260
261 let output_note_scripts: Vec<NoteScript> = transaction_request
263 .expected_output_recipients()
264 .map(|n| n.script().clone())
265 .collect();
266 self.store.upsert_note_scripts(&output_note_scripts).await?;
267
268 let block_num = if let Some(block_num) = fpi_block_num {
269 block_num
270 } else {
271 self.store.get_sync_height().await?
272 };
273
274 let account_record = self
277 .store
278 .get_account(account_id)
279 .await?
280 .ok_or(ClientError::AccountDataNotFound(account_id))?;
281 let account: Account = account_record.into();
282 data_store.mast_store().load_account_code(account.code());
283
284 let tx_args = transaction_request.into_transaction_args(tx_script);
286
287 if ignore_invalid_notes {
288 notes = self.get_valid_input_notes(account, notes, tx_args.clone()).await?;
290 }
291
292 let executed_transaction = self
294 .build_executor(&data_store)?
295 .execute_transaction(account_id, block_num, notes, tx_args)
296 .await?;
297
298 validate_executed_transaction(&executed_transaction, &output_recipients)?;
299
300 TransactionResult::new(executed_transaction, future_notes)
301 }
302
303 pub async fn prove_transaction(
305 &mut self,
306 tx_result: &TransactionResult,
307 ) -> Result<ProvenTransaction, ClientError> {
308 self.prove_transaction_with(tx_result, self.tx_prover.clone()).await
309 }
310
311 pub async fn prove_transaction_with(
313 &mut self,
314 tx_result: &TransactionResult,
315 tx_prover: Arc<dyn TransactionProver>,
316 ) -> Result<ProvenTransaction, ClientError> {
317 info!("Proving transaction...");
318
319 let proven_transaction =
320 tx_prover.prove(tx_result.executed_transaction().clone().into()).await?;
321
322 info!("Transaction proven.");
323
324 Ok(proven_transaction)
325 }
326
327 pub async fn submit_proven_transaction(
330 &mut self,
331 proven_transaction: ProvenTransaction,
332 transaction_inputs: impl Into<TransactionInputs>,
333 ) -> Result<BlockNumber, ClientError> {
334 info!("Submitting transaction to the network...");
335 let block_num = self
336 .rpc_api
337 .submit_proven_transaction(proven_transaction, transaction_inputs.into())
338 .await?;
339 info!("Transaction submitted.");
340
341 Ok(block_num)
342 }
343
344 pub async fn get_transaction_store_update(
347 &self,
348 tx_result: &TransactionResult,
349 submission_height: BlockNumber,
350 ) -> Result<TransactionStoreUpdate, ClientError> {
351 let note_updates = self.get_note_updates(submission_height, tx_result).await?;
352
353 let new_tags = note_updates
354 .updated_input_notes()
355 .filter_map(|note| {
356 let note = note.inner();
357
358 if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) =
359 note.state()
360 {
361 Some(NoteTagRecord::with_note_source(*tag, note.id()))
362 } else {
363 None
364 }
365 })
366 .collect();
367
368 Ok(TransactionStoreUpdate::new(
369 tx_result.executed_transaction().clone(),
370 submission_height,
371 note_updates,
372 tx_result.future_notes().to_vec(),
373 new_tags,
374 ))
375 }
376
377 pub async fn apply_transaction(
380 &self,
381 tx_result: &TransactionResult,
382 submission_height: BlockNumber,
383 ) -> Result<(), ClientError> {
384 let tx_update = self.get_transaction_store_update(tx_result, submission_height).await?;
385
386 self.apply_transaction_update(tx_update).await
387 }
388
389 pub async fn apply_transaction_update(
390 &self,
391 tx_update: TransactionStoreUpdate,
392 ) -> Result<(), ClientError> {
393 info!("Applying transaction to the local store...");
396
397 let executed_transaction = tx_update.executed_transaction();
398 let account_id = executed_transaction.account_id();
399 let account_record = self.try_get_account(account_id).await?;
400
401 if account_record.is_locked() {
402 return Err(ClientError::AccountLocked(account_id));
403 }
404
405 let final_commitment = executed_transaction.final_account().commitment();
406 if self.store.get_account_header_by_commitment(final_commitment).await?.is_some() {
407 return Err(ClientError::StoreError(StoreError::AccountCommitmentAlreadyExists(
408 final_commitment,
409 )));
410 }
411
412 self.store.apply_transaction(tx_update).await?;
413 info!("Transaction stored.");
414 Ok(())
415 }
416
417 pub async fn execute_program(
422 &mut self,
423 account_id: AccountId,
424 tx_script: TransactionScript,
425 advice_inputs: AdviceInputs,
426 foreign_accounts: BTreeSet<ForeignAccount>,
427 ) -> Result<[Felt; 16], ClientError> {
428 let (fpi_block_number, foreign_account_inputs) =
429 self.retrieve_foreign_account_inputs(foreign_accounts).await?;
430
431 let block_ref = if let Some(block_number) = fpi_block_number {
432 block_number
433 } else {
434 self.get_sync_height().await?
435 };
436
437 let account_record = self
438 .store
439 .get_account(account_id)
440 .await?
441 .ok_or(ClientError::AccountDataNotFound(account_id))?;
442
443 let account: Account = account_record.into();
444
445 let data_store = ClientDataStore::new(self.store.clone());
446
447 data_store.register_foreign_account_inputs(foreign_account_inputs.iter().cloned());
448
449 data_store.mast_store().load_account_code(account.code());
451
452 for fpi_account in &foreign_account_inputs {
453 data_store.mast_store().load_account_code(fpi_account.code());
454 }
455
456 Ok(self
457 .build_executor(&data_store)?
458 .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
459 .await?)
460 }
461
462 async fn get_note_updates(
475 &self,
476 submission_height: BlockNumber,
477 tx_result: &TransactionResult,
478 ) -> Result<NoteUpdateTracker, ClientError> {
479 let executed_tx = tx_result.executed_transaction();
480 let current_timestamp = self.store.get_current_timestamp();
481 let current_block_num = self.store.get_sync_height().await?;
482
483 let new_output_notes = executed_tx
485 .output_notes()
486 .iter()
487 .cloned()
488 .filter_map(|output_note| {
489 OutputNoteRecord::try_from_output_note(output_note, submission_height).ok()
490 })
491 .collect::<Vec<_>>();
492
493 let mut new_input_notes = vec![];
495 let note_screener = NoteScreener::new(self.store.clone(), self.authenticator.clone());
496
497 for note in notes_from_output(executed_tx.output_notes()) {
498 let account_relevance = note_screener.check_relevance(note).await?;
500 if !account_relevance.is_empty() {
501 let metadata = *note.metadata();
502
503 new_input_notes.push(InputNoteRecord::new(
504 note.into(),
505 current_timestamp,
506 ExpectedNoteState {
507 metadata: Some(metadata),
508 after_block_num: submission_height,
509 tag: Some(metadata.tag()),
510 }
511 .into(),
512 ));
513 }
514 }
515
516 new_input_notes.extend(tx_result.future_notes().iter().map(|(note_details, tag)| {
518 InputNoteRecord::new(
519 note_details.clone(),
520 None,
521 ExpectedNoteState {
522 metadata: None,
523 after_block_num: current_block_num,
524 tag: Some(*tag),
525 }
526 .into(),
527 )
528 }));
529
530 let consumed_note_ids =
532 executed_tx.tx_inputs().input_notes().iter().map(InputNote::id).collect();
533
534 let consumed_notes = self.get_input_notes(NoteFilter::List(consumed_note_ids)).await?;
535
536 let mut updated_input_notes = vec![];
537
538 for mut input_note_record in consumed_notes {
539 if input_note_record.consumed_locally(
540 executed_tx.account_id(),
541 executed_tx.id(),
542 self.store.get_current_timestamp(),
543 )? {
544 updated_input_notes.push(input_note_record);
545 }
546 }
547
548 Ok(NoteUpdateTracker::for_transaction_updates(
549 new_input_notes,
550 updated_input_notes,
551 new_output_notes,
552 ))
553 }
554
555 fn get_outgoing_assets(
560 transaction_request: &TransactionRequest,
561 ) -> (BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>) {
562 let mut own_notes_assets = match transaction_request.script_template() {
564 Some(TransactionScriptTemplate::SendNotes(notes)) => notes
565 .iter()
566 .map(|note| (note.id(), note.assets().clone()))
567 .collect::<BTreeMap<_, _>>(),
568 _ => BTreeMap::default(),
569 };
570 let mut output_notes_assets = transaction_request
572 .expected_output_own_notes()
573 .into_iter()
574 .map(|note| (note.id(), note.assets().clone()))
575 .collect::<BTreeMap<_, _>>();
576
577 output_notes_assets.append(&mut own_notes_assets);
579
580 let outgoing_assets =
582 output_notes_assets.values().flat_map(|note_assets| note_assets.iter());
583
584 collect_assets(outgoing_assets)
585 }
586
587 async fn get_incoming_assets(
589 &self,
590 transaction_request: &TransactionRequest,
591 ) -> Result<(BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>), TransactionRequestError>
592 {
593 let incoming_notes_ids: Vec<_> = transaction_request
595 .input_notes()
596 .iter()
597 .filter_map(|(note_id, _)| {
598 if transaction_request
599 .unauthenticated_input_notes()
600 .iter()
601 .any(|note| note.id() == *note_id)
602 {
603 None
604 } else {
605 Some(*note_id)
606 }
607 })
608 .collect();
609
610 let store_input_notes = self
611 .get_input_notes(NoteFilter::List(incoming_notes_ids))
612 .await
613 .map_err(|err| TransactionRequestError::NoteNotFound(err.to_string()))?;
614
615 let all_incoming_assets =
616 store_input_notes.iter().flat_map(|note| note.assets().iter()).chain(
617 transaction_request
618 .unauthenticated_input_notes()
619 .iter()
620 .flat_map(|note| note.assets().iter()),
621 );
622
623 Ok(collect_assets(all_incoming_assets))
624 }
625
626 async fn validate_basic_account_request(
629 &self,
630 transaction_request: &TransactionRequest,
631 account: &Account,
632 ) -> Result<(), ClientError> {
633 let (fungible_balance_map, non_fungible_set) =
635 Client::<AUTH>::get_outgoing_assets(transaction_request);
636
637 let (incoming_fungible_balance_map, incoming_non_fungible_balance_set) =
639 self.get_incoming_assets(transaction_request).await?;
640
641 for (faucet_id, amount) in fungible_balance_map {
644 let account_asset_amount = account.vault().get_balance(faucet_id).unwrap_or(0);
645 let incoming_balance = incoming_fungible_balance_map.get(&faucet_id).unwrap_or(&0);
646 if account_asset_amount + incoming_balance < amount {
647 return Err(ClientError::AssetError(
648 AssetError::FungibleAssetAmountNotSufficient {
649 minuend: account_asset_amount,
650 subtrahend: amount,
651 },
652 ));
653 }
654 }
655
656 for non_fungible in non_fungible_set {
659 match account.vault().has_non_fungible_asset(non_fungible) {
660 Ok(true) => (),
661 Ok(false) => {
662 if !incoming_non_fungible_balance_set.contains(&non_fungible) {
664 return Err(ClientError::AssetError(
665 AssetError::NonFungibleFaucetIdTypeMismatch(
666 non_fungible.faucet_id_prefix(),
667 ),
668 ));
669 }
670 },
671 _ => {
672 return Err(ClientError::AssetError(
673 AssetError::NonFungibleFaucetIdTypeMismatch(
674 non_fungible.faucet_id_prefix(),
675 ),
676 ));
677 },
678 }
679 }
680
681 Ok(())
682 }
683
684 pub async fn validate_request(
691 &mut self,
692 account_id: AccountId,
693 transaction_request: &TransactionRequest,
694 ) -> Result<(), ClientError> {
695 if let Some(max_block_number_delta) = self.max_block_number_delta {
696 let current_chain_tip =
697 self.rpc_api.get_block_header_by_number(None, false).await?.0.block_num();
698
699 if current_chain_tip > self.store.get_sync_height().await? + max_block_number_delta {
700 return Err(ClientError::RecencyConditionError(
701 "The client is too far behind the chain tip to execute the transaction",
702 ));
703 }
704 }
705
706 let account: Account = self.try_get_account(account_id).await?.into();
707
708 if account.is_faucet() {
709 Ok(())
711 } else {
712 self.validate_basic_account_request(transaction_request, &account).await
713 }
714 }
715
716 async fn get_valid_input_notes(
719 &self,
720 account: Account,
721 mut input_notes: InputNotes<InputNote>,
722 tx_args: TransactionArgs,
723 ) -> Result<InputNotes<InputNote>, ClientError> {
724 loop {
725 let data_store = ClientDataStore::new(self.store.clone());
726
727 data_store.mast_store().load_account_code(account.code());
728 let execution = NoteConsumptionChecker::new(&self.build_executor(&data_store)?)
729 .check_notes_consumability(
730 account.id(),
731 self.store.get_sync_height().await?,
732 input_notes.iter().map(|n| n.clone().into_note()).collect(),
733 tx_args.clone(),
734 )
735 .await?;
736
737 if execution.failed.is_empty() {
738 break;
739 }
740
741 let failed_note_ids: BTreeSet<NoteId> =
742 execution.failed.iter().map(|n| n.note.id()).collect();
743 let filtered_input_notes = InputNotes::new(
744 input_notes
745 .into_iter()
746 .filter(|note| !failed_note_ids.contains(¬e.id()))
747 .collect(),
748 )
749 .expect("Created from a valid input notes list");
750
751 input_notes = filtered_input_notes;
752 }
753
754 Ok(input_notes)
755 }
756
757 pub(crate) async fn get_account_interface(
759 &self,
760 account_id: AccountId,
761 ) -> Result<AccountInterface, ClientError> {
762 let account: Account = self.try_get_account(account_id).await?.into();
763
764 Ok(AccountInterface::from(&account))
765 }
766
767 async fn retrieve_foreign_account_inputs(
778 &mut self,
779 foreign_accounts: BTreeSet<ForeignAccount>,
780 ) -> Result<(Option<BlockNumber>, Vec<AccountInputs>), ClientError> {
781 if foreign_accounts.is_empty() {
782 return Ok((None, Vec::new()));
783 }
784
785 let mut return_foreign_account_inputs = Vec::with_capacity(foreign_accounts.len());
786
787 let account_ids = foreign_accounts.iter().map(ForeignAccount::account_id);
788 let known_account_codes =
789 self.store.get_foreign_account_code(account_ids.collect()).await?;
790
791 let (block_num, account_proofs) =
793 self.rpc_api.get_account_proofs(&foreign_accounts, known_account_codes).await?;
794
795 let mut account_proofs: BTreeMap<AccountId, AccountProof> =
796 account_proofs.into_iter().map(|proof| (proof.account_id(), proof)).collect();
797
798 for foreign_account in &foreign_accounts {
799 let foreign_account_inputs = match foreign_account {
800 ForeignAccount::Public(account_id, ..) => {
801 let account_proof = account_proofs
802 .remove(account_id)
803 .expect("proof was requested and received");
804
805 let foreign_account_inputs: AccountInputs = account_proof.try_into()?;
806
807 self.store
809 .upsert_foreign_account_code(
810 *account_id,
811 foreign_account_inputs.code().clone(),
812 )
813 .await?;
814
815 foreign_account_inputs
816 },
817 ForeignAccount::Private(partial_account) => {
818 let account_id = partial_account.id();
819 let (witness, _) = account_proofs
820 .remove(&account_id)
821 .expect("proof was requested and received")
822 .into_parts();
823
824 AccountInputs::new(partial_account.clone(), witness)
825 },
826 };
827
828 return_foreign_account_inputs.push(foreign_account_inputs);
829 }
830
831 if self.store.get_block_header_by_num(block_num).await?.is_none() {
833 info!(
834 "Getting current block header data to execute transaction with foreign account requirements"
835 );
836 let summary = self.sync_state().await?;
837
838 if summary.block_num != block_num {
839 let mut current_partial_mmr = self.store.get_current_partial_mmr().await?;
840 self.get_and_store_authenticated_block(block_num, &mut current_partial_mmr)
841 .await?;
842 }
843 }
844
845 Ok((Some(block_num), return_foreign_account_inputs))
846 }
847
848 pub(crate) fn build_executor<'store, 'auth, STORE: DataStore + Sync>(
851 &'auth self,
852 data_store: &'store STORE,
853 ) -> Result<TransactionExecutor<'store, 'auth, STORE, AUTH>, TransactionExecutorError> {
854 let mut executor = TransactionExecutor::new(data_store).with_options(self.exec_options)?;
855 if let Some(authenticator) = self.authenticator.as_deref() {
856 executor = executor.with_authenticator(authenticator);
857 }
858 executor = executor.with_source_manager(self.source_manager.clone());
859
860 Ok(executor)
861 }
862}
863
864fn collect_assets<'a>(
869 assets: impl Iterator<Item = &'a Asset>,
870) -> (BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>) {
871 let mut fungible_balance_map = BTreeMap::new();
872 let mut non_fungible_set = BTreeSet::new();
873
874 assets.for_each(|asset| match asset {
875 Asset::Fungible(fungible) => {
876 fungible_balance_map
877 .entry(fungible.faucet_id())
878 .and_modify(|balance| *balance += fungible.amount())
879 .or_insert(fungible.amount());
880 },
881 Asset::NonFungible(non_fungible) => {
882 non_fungible_set.insert(*non_fungible);
883 },
884 });
885
886 (fungible_balance_map, non_fungible_set)
887}
888
889pub fn notes_from_output(output_notes: &OutputNotes) -> impl Iterator<Item = &Note> {
894 output_notes
895 .iter()
896 .filter(|n| matches!(n, OutputNote::Full(_)))
897 .map(|n| match n {
898 OutputNote::Full(n) => n,
899 OutputNote::Header(_) | OutputNote::Partial(_) => {
902 todo!("For now, all details should be held in OutputNote::Fulls")
903 },
904 })
905}
906
907fn validate_executed_transaction(
910 executed_transaction: &ExecutedTransaction,
911 expected_output_recipients: &[NoteRecipient],
912) -> Result<(), ClientError> {
913 let tx_output_recipient_digests = executed_transaction
914 .output_notes()
915 .iter()
916 .filter_map(|n| n.recipient().map(NoteRecipient::digest))
917 .collect::<Vec<_>>();
918
919 let missing_recipient_digest: Vec<Word> = expected_output_recipients
920 .iter()
921 .filter_map(|recipient| {
922 (!tx_output_recipient_digests.contains(&recipient.digest()))
923 .then_some(recipient.digest())
924 })
925 .collect();
926
927 if !missing_recipient_digest.is_empty() {
928 return Err(ClientError::MissingOutputRecipients(missing_recipient_digest));
929 }
930
931 Ok(())
932}