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
262 .expected_output_own_notes()
263 .iter()
264 .map(|n| n.script().clone())
265 .collect();
266 self.store.upsert_note_scripts(&output_note_scripts).await?;
267
268 let tx_args = transaction_request.into_transaction_args(tx_script);
269
270 let block_num = if let Some(block_num) = fpi_block_num {
271 block_num
272 } else {
273 self.store.get_sync_height().await?
274 };
275
276 let account_record = self
278 .store
279 .get_account(account_id)
280 .await?
281 .ok_or(ClientError::AccountDataNotFound(account_id))?;
282 let account: Account = account_record.into();
283 data_store.mast_store().load_account_code(account.code());
284
285 if ignore_invalid_notes {
286 notes = self.get_valid_input_notes(account, notes, tx_args.clone()).await?;
288 }
289
290 let executed_transaction = self
292 .build_executor(&data_store)?
293 .execute_transaction(account_id, block_num, notes, tx_args)
294 .await?;
295
296 validate_executed_transaction(&executed_transaction, &output_recipients)?;
297
298 TransactionResult::new(executed_transaction, future_notes)
299 }
300
301 pub async fn prove_transaction(
303 &mut self,
304 tx_result: &TransactionResult,
305 ) -> Result<ProvenTransaction, ClientError> {
306 self.prove_transaction_with(tx_result, self.tx_prover.clone()).await
307 }
308
309 pub async fn prove_transaction_with(
311 &mut self,
312 tx_result: &TransactionResult,
313 tx_prover: Arc<dyn TransactionProver>,
314 ) -> Result<ProvenTransaction, ClientError> {
315 info!("Proving transaction...");
316
317 let proven_transaction =
318 tx_prover.prove(tx_result.executed_transaction().clone().into()).await?;
319
320 info!("Transaction proven.");
321
322 Ok(proven_transaction)
323 }
324
325 pub async fn submit_proven_transaction(
328 &mut self,
329 proven_transaction: ProvenTransaction,
330 transaction_inputs: impl Into<TransactionInputs>,
331 ) -> Result<BlockNumber, ClientError> {
332 info!("Submitting transaction to the network...");
333 let block_num = self
334 .rpc_api
335 .submit_proven_transaction(proven_transaction, transaction_inputs.into())
336 .await?;
337 info!("Transaction submitted.");
338
339 Ok(block_num)
340 }
341
342 pub async fn get_transaction_store_update(
345 &self,
346 tx_result: &TransactionResult,
347 submission_height: BlockNumber,
348 ) -> Result<TransactionStoreUpdate, ClientError> {
349 let note_updates = self.get_note_updates(submission_height, tx_result).await?;
350
351 let new_tags = note_updates
352 .updated_input_notes()
353 .filter_map(|note| {
354 let note = note.inner();
355
356 if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) =
357 note.state()
358 {
359 Some(NoteTagRecord::with_note_source(*tag, note.id()))
360 } else {
361 None
362 }
363 })
364 .collect();
365
366 Ok(TransactionStoreUpdate::new(
367 tx_result.executed_transaction().clone(),
368 submission_height,
369 note_updates,
370 tx_result.future_notes().to_vec(),
371 new_tags,
372 ))
373 }
374
375 pub async fn apply_transaction(
378 &self,
379 tx_result: &TransactionResult,
380 submission_height: BlockNumber,
381 ) -> Result<(), ClientError> {
382 let tx_update = self.get_transaction_store_update(tx_result, submission_height).await?;
383
384 self.apply_transaction_update(tx_update).await
385 }
386
387 pub async fn apply_transaction_update(
388 &self,
389 tx_update: TransactionStoreUpdate,
390 ) -> Result<(), ClientError> {
391 info!("Applying transaction to the local store...");
394
395 let executed_transaction = tx_update.executed_transaction();
396 let account_id = executed_transaction.account_id();
397 let account_record = self.try_get_account(account_id).await?;
398
399 if account_record.is_locked() {
400 return Err(ClientError::AccountLocked(account_id));
401 }
402
403 let final_commitment = executed_transaction.final_account().commitment();
404 if self.store.get_account_header_by_commitment(final_commitment).await?.is_some() {
405 return Err(ClientError::StoreError(StoreError::AccountCommitmentAlreadyExists(
406 final_commitment,
407 )));
408 }
409
410 self.store.apply_transaction(tx_update).await?;
411 info!("Transaction stored.");
412 Ok(())
413 }
414
415 pub async fn execute_program(
420 &mut self,
421 account_id: AccountId,
422 tx_script: TransactionScript,
423 advice_inputs: AdviceInputs,
424 foreign_accounts: BTreeSet<ForeignAccount>,
425 ) -> Result<[Felt; 16], ClientError> {
426 let (fpi_block_number, foreign_account_inputs) =
427 self.retrieve_foreign_account_inputs(foreign_accounts).await?;
428
429 let block_ref = if let Some(block_number) = fpi_block_number {
430 block_number
431 } else {
432 self.get_sync_height().await?
433 };
434
435 let account_record = self
436 .store
437 .get_account(account_id)
438 .await?
439 .ok_or(ClientError::AccountDataNotFound(account_id))?;
440
441 let account: Account = account_record.into();
442
443 let data_store = ClientDataStore::new(self.store.clone());
444
445 data_store.register_foreign_account_inputs(foreign_account_inputs.iter().cloned());
446
447 data_store.mast_store().load_account_code(account.code());
449
450 for fpi_account in &foreign_account_inputs {
451 data_store.mast_store().load_account_code(fpi_account.code());
452 }
453
454 Ok(self
455 .build_executor(&data_store)?
456 .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
457 .await?)
458 }
459
460 async fn get_note_updates(
473 &self,
474 submission_height: BlockNumber,
475 tx_result: &TransactionResult,
476 ) -> Result<NoteUpdateTracker, ClientError> {
477 let executed_tx = tx_result.executed_transaction();
478 let current_timestamp = self.store.get_current_timestamp();
479 let current_block_num = self.store.get_sync_height().await?;
480
481 let new_output_notes = executed_tx
483 .output_notes()
484 .iter()
485 .cloned()
486 .filter_map(|output_note| {
487 OutputNoteRecord::try_from_output_note(output_note, submission_height).ok()
488 })
489 .collect::<Vec<_>>();
490
491 let mut new_input_notes = vec![];
493 let note_screener = NoteScreener::new(self.store.clone(), self.authenticator.clone());
494
495 for note in notes_from_output(executed_tx.output_notes()) {
496 let account_relevance = note_screener.check_relevance(note).await?;
498 if !account_relevance.is_empty() {
499 let metadata = *note.metadata();
500
501 new_input_notes.push(InputNoteRecord::new(
502 note.into(),
503 current_timestamp,
504 ExpectedNoteState {
505 metadata: Some(metadata),
506 after_block_num: submission_height,
507 tag: Some(metadata.tag()),
508 }
509 .into(),
510 ));
511 }
512 }
513
514 new_input_notes.extend(tx_result.future_notes().iter().map(|(note_details, tag)| {
516 InputNoteRecord::new(
517 note_details.clone(),
518 None,
519 ExpectedNoteState {
520 metadata: None,
521 after_block_num: current_block_num,
522 tag: Some(*tag),
523 }
524 .into(),
525 )
526 }));
527
528 let consumed_note_ids =
530 executed_tx.tx_inputs().input_notes().iter().map(InputNote::id).collect();
531
532 let consumed_notes = self.get_input_notes(NoteFilter::List(consumed_note_ids)).await?;
533
534 let mut updated_input_notes = vec![];
535
536 for mut input_note_record in consumed_notes {
537 if input_note_record.consumed_locally(
538 executed_tx.account_id(),
539 executed_tx.id(),
540 self.store.get_current_timestamp(),
541 )? {
542 updated_input_notes.push(input_note_record);
543 }
544 }
545
546 Ok(NoteUpdateTracker::for_transaction_updates(
547 new_input_notes,
548 updated_input_notes,
549 new_output_notes,
550 ))
551 }
552
553 fn get_outgoing_assets(
558 transaction_request: &TransactionRequest,
559 ) -> (BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>) {
560 let mut own_notes_assets = match transaction_request.script_template() {
562 Some(TransactionScriptTemplate::SendNotes(notes)) => notes
563 .iter()
564 .map(|note| (note.id(), note.assets().clone()))
565 .collect::<BTreeMap<_, _>>(),
566 _ => BTreeMap::default(),
567 };
568 let mut output_notes_assets = transaction_request
570 .expected_output_own_notes()
571 .into_iter()
572 .map(|note| (note.id(), note.assets().clone()))
573 .collect::<BTreeMap<_, _>>();
574
575 output_notes_assets.append(&mut own_notes_assets);
577
578 let outgoing_assets =
580 output_notes_assets.values().flat_map(|note_assets| note_assets.iter());
581
582 collect_assets(outgoing_assets)
583 }
584
585 async fn get_incoming_assets(
587 &self,
588 transaction_request: &TransactionRequest,
589 ) -> Result<(BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>), TransactionRequestError>
590 {
591 let incoming_notes_ids: Vec<_> = transaction_request
593 .input_notes()
594 .iter()
595 .filter_map(|(note_id, _)| {
596 if transaction_request
597 .unauthenticated_input_notes()
598 .iter()
599 .any(|note| note.id() == *note_id)
600 {
601 None
602 } else {
603 Some(*note_id)
604 }
605 })
606 .collect();
607
608 let store_input_notes = self
609 .get_input_notes(NoteFilter::List(incoming_notes_ids))
610 .await
611 .map_err(|err| TransactionRequestError::NoteNotFound(err.to_string()))?;
612
613 let all_incoming_assets =
614 store_input_notes.iter().flat_map(|note| note.assets().iter()).chain(
615 transaction_request
616 .unauthenticated_input_notes()
617 .iter()
618 .flat_map(|note| note.assets().iter()),
619 );
620
621 Ok(collect_assets(all_incoming_assets))
622 }
623
624 async fn validate_basic_account_request(
627 &self,
628 transaction_request: &TransactionRequest,
629 account: &Account,
630 ) -> Result<(), ClientError> {
631 let (fungible_balance_map, non_fungible_set) =
633 Client::<AUTH>::get_outgoing_assets(transaction_request);
634
635 let (incoming_fungible_balance_map, incoming_non_fungible_balance_set) =
637 self.get_incoming_assets(transaction_request).await?;
638
639 for (faucet_id, amount) in fungible_balance_map {
642 let account_asset_amount = account.vault().get_balance(faucet_id).unwrap_or(0);
643 let incoming_balance = incoming_fungible_balance_map.get(&faucet_id).unwrap_or(&0);
644 if account_asset_amount + incoming_balance < amount {
645 return Err(ClientError::AssetError(
646 AssetError::FungibleAssetAmountNotSufficient {
647 minuend: account_asset_amount,
648 subtrahend: amount,
649 },
650 ));
651 }
652 }
653
654 for non_fungible in non_fungible_set {
657 match account.vault().has_non_fungible_asset(non_fungible) {
658 Ok(true) => (),
659 Ok(false) => {
660 if !incoming_non_fungible_balance_set.contains(&non_fungible) {
662 return Err(ClientError::AssetError(
663 AssetError::NonFungibleFaucetIdTypeMismatch(
664 non_fungible.faucet_id_prefix(),
665 ),
666 ));
667 }
668 },
669 _ => {
670 return Err(ClientError::AssetError(
671 AssetError::NonFungibleFaucetIdTypeMismatch(
672 non_fungible.faucet_id_prefix(),
673 ),
674 ));
675 },
676 }
677 }
678
679 Ok(())
680 }
681
682 pub async fn validate_request(
689 &mut self,
690 account_id: AccountId,
691 transaction_request: &TransactionRequest,
692 ) -> Result<(), ClientError> {
693 if let Some(max_block_number_delta) = self.max_block_number_delta {
694 let current_chain_tip =
695 self.rpc_api.get_block_header_by_number(None, false).await?.0.block_num();
696
697 if current_chain_tip > self.store.get_sync_height().await? + max_block_number_delta {
698 return Err(ClientError::RecencyConditionError(
699 "The client is too far behind the chain tip to execute the transaction",
700 ));
701 }
702 }
703
704 let account: Account = self.try_get_account(account_id).await?.into();
705
706 if account.is_faucet() {
707 Ok(())
709 } else {
710 self.validate_basic_account_request(transaction_request, &account).await
711 }
712 }
713
714 async fn get_valid_input_notes(
717 &self,
718 account: Account,
719 mut input_notes: InputNotes<InputNote>,
720 tx_args: TransactionArgs,
721 ) -> Result<InputNotes<InputNote>, ClientError> {
722 loop {
723 let data_store = ClientDataStore::new(self.store.clone());
724
725 data_store.mast_store().load_account_code(account.code());
726 let execution = NoteConsumptionChecker::new(&self.build_executor(&data_store)?)
727 .check_notes_consumability(
728 account.id(),
729 self.store.get_sync_height().await?,
730 input_notes.iter().map(|n| n.clone().into_note()).collect(),
731 tx_args.clone(),
732 )
733 .await?;
734
735 if execution.failed.is_empty() {
736 break;
737 }
738
739 let failed_note_ids: BTreeSet<NoteId> =
740 execution.failed.iter().map(|n| n.note.id()).collect();
741 let filtered_input_notes = InputNotes::new(
742 input_notes
743 .into_iter()
744 .filter(|note| !failed_note_ids.contains(¬e.id()))
745 .collect(),
746 )
747 .expect("Created from a valid input notes list");
748
749 input_notes = filtered_input_notes;
750 }
751
752 Ok(input_notes)
753 }
754
755 pub(crate) async fn get_account_interface(
757 &self,
758 account_id: AccountId,
759 ) -> Result<AccountInterface, ClientError> {
760 let account: Account = self.try_get_account(account_id).await?.into();
761
762 Ok(AccountInterface::from(&account))
763 }
764
765 async fn retrieve_foreign_account_inputs(
776 &mut self,
777 foreign_accounts: BTreeSet<ForeignAccount>,
778 ) -> Result<(Option<BlockNumber>, Vec<AccountInputs>), ClientError> {
779 if foreign_accounts.is_empty() {
780 return Ok((None, Vec::new()));
781 }
782
783 let mut return_foreign_account_inputs = Vec::with_capacity(foreign_accounts.len());
784
785 let account_ids = foreign_accounts.iter().map(ForeignAccount::account_id);
786 let known_account_codes =
787 self.store.get_foreign_account_code(account_ids.collect()).await?;
788
789 let (block_num, account_proofs) =
791 self.rpc_api.get_account_proofs(&foreign_accounts, known_account_codes).await?;
792
793 let mut account_proofs: BTreeMap<AccountId, AccountProof> =
794 account_proofs.into_iter().map(|proof| (proof.account_id(), proof)).collect();
795
796 for foreign_account in &foreign_accounts {
797 let foreign_account_inputs = match foreign_account {
798 ForeignAccount::Public(account_id, ..) => {
799 let account_proof = account_proofs
800 .remove(account_id)
801 .expect("proof was requested and received");
802
803 let foreign_account_inputs: AccountInputs = account_proof.try_into()?;
804
805 self.store
807 .upsert_foreign_account_code(
808 *account_id,
809 foreign_account_inputs.code().clone(),
810 )
811 .await?;
812
813 foreign_account_inputs
814 },
815 ForeignAccount::Private(partial_account) => {
816 let account_id = partial_account.id();
817 let (witness, _) = account_proofs
818 .remove(&account_id)
819 .expect("proof was requested and received")
820 .into_parts();
821
822 AccountInputs::new(partial_account.clone(), witness)
823 },
824 };
825
826 return_foreign_account_inputs.push(foreign_account_inputs);
827 }
828
829 if self.store.get_block_header_by_num(block_num).await?.is_none() {
831 info!(
832 "Getting current block header data to execute transaction with foreign account requirements"
833 );
834 let summary = self.sync_state().await?;
835
836 if summary.block_num != block_num {
837 let mut current_partial_mmr = self.store.get_current_partial_mmr().await?;
838 self.get_and_store_authenticated_block(block_num, &mut current_partial_mmr)
839 .await?;
840 }
841 }
842
843 Ok((Some(block_num), return_foreign_account_inputs))
844 }
845
846 pub(crate) fn build_executor<'store, 'auth, STORE: DataStore + Sync>(
849 &'auth self,
850 data_store: &'store STORE,
851 ) -> Result<TransactionExecutor<'store, 'auth, STORE, AUTH>, TransactionExecutorError> {
852 let mut executor = TransactionExecutor::new(data_store).with_options(self.exec_options)?;
853 if let Some(authenticator) = self.authenticator.as_deref() {
854 executor = executor.with_authenticator(authenticator);
855 }
856 executor = executor.with_source_manager(self.source_manager.clone());
857
858 Ok(executor)
859 }
860}
861
862fn collect_assets<'a>(
867 assets: impl Iterator<Item = &'a Asset>,
868) -> (BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>) {
869 let mut fungible_balance_map = BTreeMap::new();
870 let mut non_fungible_set = BTreeSet::new();
871
872 assets.for_each(|asset| match asset {
873 Asset::Fungible(fungible) => {
874 fungible_balance_map
875 .entry(fungible.faucet_id())
876 .and_modify(|balance| *balance += fungible.amount())
877 .or_insert(fungible.amount());
878 },
879 Asset::NonFungible(non_fungible) => {
880 non_fungible_set.insert(*non_fungible);
881 },
882 });
883
884 (fungible_balance_map, non_fungible_set)
885}
886
887pub fn notes_from_output(output_notes: &OutputNotes) -> impl Iterator<Item = &Note> {
892 output_notes
893 .iter()
894 .filter(|n| matches!(n, OutputNote::Full(_)))
895 .map(|n| match n {
896 OutputNote::Full(n) => n,
897 OutputNote::Header(_) | OutputNote::Partial(_) => {
900 todo!("For now, all details should be held in OutputNote::Fulls")
901 },
902 })
903}
904
905fn validate_executed_transaction(
908 executed_transaction: &ExecutedTransaction,
909 expected_output_recipients: &[NoteRecipient],
910) -> Result<(), ClientError> {
911 let tx_output_recipient_digests = executed_transaction
912 .output_notes()
913 .iter()
914 .filter_map(|n| n.recipient().map(NoteRecipient::digest))
915 .collect::<Vec<_>>();
916
917 let missing_recipient_digest: Vec<Word> = expected_output_recipients
918 .iter()
919 .filter_map(|recipient| {
920 (!tx_output_recipient_digests.contains(&recipient.digest()))
921 .then_some(recipient.digest())
922 })
923 .collect();
924
925 if !missing_recipient_digest.is_empty() {
926 return Err(ClientError::MissingOutputRecipients(missing_recipient_digest));
927 }
928
929 Ok(())
930}