1use alloc::collections::{BTreeMap, BTreeSet};
66use alloc::sync::Arc;
67use alloc::vec::Vec;
68
69use miden_protocol::account::{Account, AccountId};
70use miden_protocol::asset::NonFungibleAsset;
71use miden_protocol::block::BlockNumber;
72use miden_protocol::errors::AssetError;
73use miden_protocol::note::{Note, NoteDetails, NoteId, NoteRecipient, NoteScript, NoteTag};
74use miden_protocol::transaction::AccountInputs;
75use miden_protocol::{Felt, Word};
76use miden_standards::account::interface::AccountInterfaceExt;
77use miden_tx::{DataStore, NoteConsumptionChecker, TransactionExecutor};
78use tracing::info;
79
80use super::Client;
81use crate::ClientError;
82use crate::note::NoteUpdateTracker;
83use crate::rpc::AccountStateAt;
84use crate::store::data_store::ClientDataStore;
85use crate::store::input_note_states::ExpectedNoteState;
86use crate::store::{
87 InputNoteRecord,
88 InputNoteState,
89 NoteFilter,
90 OutputNoteRecord,
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;
111use request::account_proof_into_inputs;
112pub use request::{
113 ForeignAccount,
114 NoteArgs,
115 PaymentNoteDescription,
116 SwapTransactionData,
117 TransactionRequest,
118 TransactionRequestBuilder,
119 TransactionRequestError,
120 TransactionScriptTemplate,
121};
122
123mod result;
124pub use miden_protocol::transaction::{
127 ExecutedTransaction,
128 InputNote,
129 InputNotes,
130 OutputNote,
131 OutputNotes,
132 ProvenTransaction,
133 TransactionArgs,
134 TransactionId,
135 TransactionInputs,
136 TransactionKernel,
137 TransactionScript,
138 TransactionSummary,
139};
140pub use miden_protocol::vm::{AdviceInputs, AdviceMap};
141pub use miden_standards::account::interface::{AccountComponentInterface, AccountInterface};
142pub use miden_tx::auth::TransactionAuthenticator;
143pub use miden_tx::{
144 DataStoreError,
145 LocalTransactionProver,
146 ProvingOptions,
147 TransactionExecutorError,
148 TransactionProverError,
149};
150pub use result::TransactionResult;
151
152impl<AUTH> Client<AUTH>
154where
155 AUTH: TransactionAuthenticator + Sync + 'static,
156{
157 pub async fn get_transactions(
162 &self,
163 filter: TransactionFilter,
164 ) -> Result<Vec<TransactionRecord>, ClientError> {
165 self.store.get_transactions(filter).await.map_err(Into::into)
166 }
167
168 pub async fn submit_new_transaction(
181 &mut self,
182 account_id: AccountId,
183 transaction_request: TransactionRequest,
184 ) -> Result<TransactionId, ClientError> {
185 let prover = self.tx_prover.clone();
186 self.submit_new_transaction_with_prover(account_id, transaction_request, prover)
187 .await
188 }
189
190 pub async fn submit_new_transaction_with_prover(
201 &mut self,
202 account_id: AccountId,
203 transaction_request: TransactionRequest,
204 tx_prover: Arc<dyn TransactionProver>,
205 ) -> Result<TransactionId, ClientError> {
206 let tx_result = self.execute_transaction(account_id, transaction_request).await?;
207 let tx_id = tx_result.executed_transaction().id();
208
209 let proven_transaction = self.prove_transaction_with(&tx_result, tx_prover).await?;
210 let submission_height =
211 self.submit_proven_transaction(proven_transaction, &tx_result).await?;
212
213 self.apply_transaction(&tx_result, submission_height).await?;
214
215 Ok(tx_id)
216 }
217
218 pub async fn execute_transaction(
232 &mut self,
233 account_id: AccountId,
234 transaction_request: TransactionRequest,
235 ) -> Result<TransactionResult, ClientError> {
236 self.validate_request(account_id, &transaction_request).await?;
238
239 let mut stored_note_records = self
241 .store
242 .get_input_notes(NoteFilter::List(transaction_request.input_note_ids().collect()))
243 .await?;
244
245 for note in &stored_note_records {
247 if note.is_consumed() {
248 return Err(ClientError::TransactionRequestError(
249 TransactionRequestError::InputNoteAlreadyConsumed(note.id()),
250 ));
251 }
252 }
253
254 stored_note_records.retain(InputNoteRecord::is_authenticated);
256
257 let authenticated_note_ids =
258 stored_note_records.iter().map(InputNoteRecord::id).collect::<Vec<_>>();
259
260 let unauthenticated_input_notes = transaction_request
265 .input_notes()
266 .iter()
267 .filter(|n| !authenticated_note_ids.contains(&n.id()))
268 .cloned()
269 .map(Into::into)
270 .collect::<Vec<_>>();
271
272 self.store.upsert_input_notes(&unauthenticated_input_notes).await?;
273
274 let mut notes = transaction_request.build_input_notes(stored_note_records)?;
275
276 let output_recipients =
277 transaction_request.expected_output_recipients().cloned().collect::<Vec<_>>();
278
279 let future_notes: Vec<(NoteDetails, NoteTag)> =
280 transaction_request.expected_future_notes().cloned().collect();
281
282 let tx_script = transaction_request
283 .build_transaction_script(&self.get_account_interface(account_id).await?)?;
284
285 let foreign_accounts = transaction_request.foreign_accounts().clone();
286
287 let (fpi_block_num, foreign_account_inputs) =
289 self.retrieve_foreign_account_inputs(foreign_accounts).await?;
290
291 let ignore_invalid_notes = transaction_request.ignore_invalid_input_notes();
292
293 let data_store = ClientDataStore::new(self.store.clone());
294 data_store.register_foreign_account_inputs(foreign_account_inputs.iter().cloned());
295 for fpi_account in &foreign_account_inputs {
296 data_store.mast_store().load_account_code(fpi_account.code());
297 }
298
299 let output_note_scripts: Vec<NoteScript> = transaction_request
301 .expected_output_recipients()
302 .map(|n| n.script().clone())
303 .collect();
304 self.store.upsert_note_scripts(&output_note_scripts).await?;
305
306 let block_num = if let Some(block_num) = fpi_block_num {
307 block_num
308 } else {
309 self.store.get_sync_height().await?
310 };
311
312 let account_record = self
315 .store
316 .get_account(account_id)
317 .await?
318 .ok_or(ClientError::AccountDataNotFound(account_id))?;
319 let account: Account = account_record.try_into()?;
320 data_store.mast_store().load_account_code(account.code());
321
322 let tx_args = transaction_request.into_transaction_args(tx_script);
324
325 if ignore_invalid_notes {
326 notes = self.get_valid_input_notes(account, notes, tx_args.clone()).await?;
328 }
329
330 let executed_transaction = self
332 .build_executor(&data_store)?
333 .execute_transaction(account_id, block_num, notes, tx_args)
334 .await?;
335
336 validate_executed_transaction(&executed_transaction, &output_recipients)?;
337 TransactionResult::new(executed_transaction, future_notes)
338 }
339
340 pub async fn prove_transaction(
342 &mut self,
343 tx_result: &TransactionResult,
344 ) -> Result<ProvenTransaction, ClientError> {
345 self.prove_transaction_with(tx_result, self.tx_prover.clone()).await
346 }
347
348 pub async fn prove_transaction_with(
350 &mut self,
351 tx_result: &TransactionResult,
352 tx_prover: Arc<dyn TransactionProver>,
353 ) -> Result<ProvenTransaction, ClientError> {
354 info!("Proving transaction...");
355
356 let proven_transaction =
357 tx_prover.prove(tx_result.executed_transaction().clone().into()).await?;
358
359 info!("Transaction proven.");
360
361 Ok(proven_transaction)
362 }
363
364 pub async fn submit_proven_transaction(
367 &mut self,
368 proven_transaction: ProvenTransaction,
369 transaction_inputs: impl Into<TransactionInputs>,
370 ) -> Result<BlockNumber, ClientError> {
371 info!("Submitting transaction to the network...");
372 let block_num = self
373 .rpc_api
374 .submit_proven_transaction(proven_transaction, transaction_inputs.into())
375 .await?;
376 info!("Transaction submitted.");
377
378 Ok(block_num)
379 }
380
381 pub async fn get_transaction_store_update(
384 &self,
385 tx_result: &TransactionResult,
386 submission_height: BlockNumber,
387 ) -> Result<TransactionStoreUpdate, ClientError> {
388 let note_updates = self.get_note_updates(submission_height, tx_result).await?;
389
390 let new_tags = note_updates
391 .updated_input_notes()
392 .filter_map(|note| {
393 let note = note.inner();
394
395 if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) =
396 note.state()
397 {
398 Some(NoteTagRecord::with_note_source(*tag, note.id()))
399 } else {
400 None
401 }
402 })
403 .collect();
404
405 Ok(TransactionStoreUpdate::new(
406 tx_result.executed_transaction().clone(),
407 submission_height,
408 note_updates,
409 tx_result.future_notes().to_vec(),
410 new_tags,
411 ))
412 }
413
414 pub async fn apply_transaction(
417 &self,
418 tx_result: &TransactionResult,
419 submission_height: BlockNumber,
420 ) -> Result<(), ClientError> {
421 let tx_update = self.get_transaction_store_update(tx_result, submission_height).await?;
422
423 self.apply_transaction_update(tx_update).await
424 }
425
426 pub async fn apply_transaction_update(
427 &self,
428 tx_update: TransactionStoreUpdate,
429 ) -> Result<(), ClientError> {
430 info!("Applying transaction to the local store...");
433
434 let executed_transaction = tx_update.executed_transaction();
435 let account_id = executed_transaction.account_id();
436
437 if self.account_reader(account_id).status().await?.is_locked() {
438 return Err(ClientError::AccountLocked(account_id));
439 }
440
441 self.store.apply_transaction(tx_update).await?;
442 info!("Transaction stored.");
443 Ok(())
444 }
445
446 pub async fn execute_program(
451 &mut self,
452 account_id: AccountId,
453 tx_script: TransactionScript,
454 advice_inputs: AdviceInputs,
455 foreign_accounts: BTreeSet<ForeignAccount>,
456 ) -> Result<[Felt; 16], ClientError> {
457 let (fpi_block_number, foreign_account_inputs) =
458 self.retrieve_foreign_account_inputs(foreign_accounts).await?;
459
460 let block_ref = if let Some(block_number) = fpi_block_number {
461 block_number
462 } else {
463 self.get_sync_height().await?
464 };
465
466 let account_record = self
467 .store
468 .get_account(account_id)
469 .await?
470 .ok_or(ClientError::AccountDataNotFound(account_id))?;
471
472 let account: Account = account_record.try_into()?;
473
474 let data_store = ClientDataStore::new(self.store.clone());
475
476 data_store.register_foreign_account_inputs(foreign_account_inputs.iter().cloned());
477
478 data_store.mast_store().load_account_code(account.code());
480
481 for fpi_account in &foreign_account_inputs {
482 data_store.mast_store().load_account_code(fpi_account.code());
483 }
484
485 Ok(self
486 .build_executor(&data_store)?
487 .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
488 .await?)
489 }
490
491 async fn get_note_updates(
504 &self,
505 submission_height: BlockNumber,
506 tx_result: &TransactionResult,
507 ) -> Result<NoteUpdateTracker, ClientError> {
508 let executed_tx = tx_result.executed_transaction();
509 let current_timestamp = self.store.get_current_timestamp();
510 let current_block_num = self.store.get_sync_height().await?;
511
512 let new_output_notes = executed_tx
514 .output_notes()
515 .iter()
516 .cloned()
517 .filter_map(|output_note| {
518 OutputNoteRecord::try_from_output_note(output_note, submission_height).ok()
519 })
520 .collect::<Vec<_>>();
521
522 let mut new_input_notes = vec![];
524 let output_notes =
525 notes_from_output(executed_tx.output_notes()).cloned().collect::<Vec<_>>();
526 let note_screener = self.note_screener();
527 let output_note_relevances = note_screener.can_consume_batch(&output_notes).await?;
528
529 for note in output_notes {
530 if output_note_relevances.contains_key(¬e.id()) {
531 let metadata = note.metadata().clone();
532 let tag = metadata.tag();
533
534 new_input_notes.push(InputNoteRecord::new(
535 note.into(),
536 current_timestamp,
537 ExpectedNoteState {
538 metadata: Some(metadata),
539 after_block_num: submission_height,
540 tag: Some(tag),
541 }
542 .into(),
543 ));
544 }
545 }
546
547 new_input_notes.extend(tx_result.future_notes().iter().map(|(note_details, tag)| {
549 InputNoteRecord::new(
550 note_details.clone(),
551 None,
552 ExpectedNoteState {
553 metadata: None,
554 after_block_num: current_block_num,
555 tag: Some(*tag),
556 }
557 .into(),
558 )
559 }));
560
561 let consumed_note_ids =
563 executed_tx.tx_inputs().input_notes().iter().map(InputNote::id).collect();
564
565 let consumed_notes = self.get_input_notes(NoteFilter::List(consumed_note_ids)).await?;
566
567 let mut updated_input_notes = vec![];
568
569 for mut input_note_record in consumed_notes {
570 if input_note_record.consumed_locally(
571 executed_tx.account_id(),
572 executed_tx.id(),
573 self.store.get_current_timestamp(),
574 )? {
575 updated_input_notes.push(input_note_record);
576 }
577 }
578
579 Ok(NoteUpdateTracker::for_transaction_updates(
580 new_input_notes,
581 updated_input_notes,
582 new_output_notes,
583 ))
584 }
585
586 pub async fn validate_request(
593 &mut self,
594 account_id: AccountId,
595 transaction_request: &TransactionRequest,
596 ) -> Result<(), ClientError> {
597 if let Some(max_block_number_delta) = self.max_block_number_delta {
598 let current_chain_tip =
599 self.rpc_api.get_block_header_by_number(None, false).await?.0.block_num();
600
601 if current_chain_tip > self.store.get_sync_height().await? + max_block_number_delta {
602 return Err(ClientError::RecencyConditionError(
603 "The client is too far behind the chain tip to execute the transaction",
604 ));
605 }
606 }
607
608 let account = self.try_get_account(account_id).await?;
609 if account.is_faucet() {
610 Ok(())
612 } else {
613 validate_basic_account_request(transaction_request, &account)
614 }
615 }
616
617 async fn get_valid_input_notes(
620 &self,
621 account: Account,
622 mut input_notes: InputNotes<InputNote>,
623 tx_args: TransactionArgs,
624 ) -> Result<InputNotes<InputNote>, ClientError> {
625 loop {
626 let data_store = ClientDataStore::new(self.store.clone());
627
628 data_store.mast_store().load_account_code(account.code());
629 let execution = NoteConsumptionChecker::new(&self.build_executor(&data_store)?)
630 .check_notes_consumability(
631 account.id(),
632 self.store.get_sync_height().await?,
633 input_notes.iter().map(|n| n.clone().into_note()).collect(),
634 tx_args.clone(),
635 )
636 .await?;
637
638 if execution.failed.is_empty() {
639 break;
640 }
641
642 let failed_note_ids: BTreeSet<NoteId> =
643 execution.failed.iter().map(|n| n.note.id()).collect();
644 let filtered_input_notes = InputNotes::new(
645 input_notes
646 .into_iter()
647 .filter(|note| !failed_note_ids.contains(¬e.id()))
648 .collect(),
649 )
650 .expect("Created from a valid input notes list");
651
652 input_notes = filtered_input_notes;
653 }
654
655 Ok(input_notes)
656 }
657
658 pub(crate) async fn get_account_interface(
660 &self,
661 account_id: AccountId,
662 ) -> Result<AccountInterface, ClientError> {
663 let account = self.try_get_account(account_id).await?;
664 Ok(AccountInterface::from_account(&account))
665 }
666
667 async fn retrieve_foreign_account_inputs(
678 &mut self,
679 foreign_accounts: BTreeSet<ForeignAccount>,
680 ) -> Result<(Option<BlockNumber>, Vec<AccountInputs>), ClientError> {
681 if foreign_accounts.is_empty() {
682 return Ok((None, Vec::new()));
683 }
684
685 let block_num = self.get_sync_height().await?;
686 let mut return_foreign_account_inputs = Vec::with_capacity(foreign_accounts.len());
687
688 for foreign_account in foreign_accounts {
689 let account_id = foreign_account.account_id();
690 let storage_requirements = foreign_account.storage_slot_requirements();
691 let known_account_code = self
692 .store
693 .get_foreign_account_code(vec![account_id])
694 .await?
695 .pop_first()
696 .map(|(_, code)| code);
697
698 let (_, account_proof) = self
699 .rpc_api
700 .get_account_proof(
701 account_id,
702 storage_requirements,
703 AccountStateAt::Block(block_num),
704 known_account_code,
705 )
706 .await?;
707 let foreign_account_inputs = match foreign_account {
708 ForeignAccount::Public(account_id, ..) => {
709 let foreign_account_inputs: AccountInputs =
710 account_proof_into_inputs(account_proof)?;
711
712 self.store
714 .upsert_foreign_account_code(
715 account_id,
716 foreign_account_inputs.code().clone(),
717 )
718 .await?;
719
720 foreign_account_inputs
721 },
722 ForeignAccount::Private(partial_account) => {
723 let (witness, _) = account_proof.into_parts();
724
725 AccountInputs::new(partial_account, witness)
726 },
727 };
728
729 return_foreign_account_inputs.push(foreign_account_inputs);
730 }
731
732 Ok((Some(block_num), return_foreign_account_inputs))
733 }
734
735 pub(crate) fn build_executor<'store, 'auth, STORE: DataStore + Sync>(
738 &'auth self,
739 data_store: &'store STORE,
740 ) -> Result<TransactionExecutor<'store, 'auth, STORE, AUTH>, TransactionExecutorError> {
741 let mut executor = TransactionExecutor::new(data_store).with_options(self.exec_options)?;
742 if let Some(authenticator) = self.authenticator.as_deref() {
743 executor = executor.with_authenticator(authenticator);
744 }
745 executor = executor.with_source_manager(self.source_manager.clone());
746
747 Ok(executor)
748 }
749}
750
751fn get_outgoing_assets(
759 transaction_request: &TransactionRequest,
760) -> (BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>) {
761 let mut own_notes_assets = match transaction_request.script_template() {
763 Some(TransactionScriptTemplate::SendNotes(notes)) => notes
764 .iter()
765 .map(|note| (note.id(), note.assets().clone()))
766 .collect::<BTreeMap<_, _>>(),
767 _ => BTreeMap::default(),
768 };
769 let mut output_notes_assets = transaction_request
771 .expected_output_own_notes()
772 .into_iter()
773 .map(|note| (note.id(), note.assets().clone()))
774 .collect::<BTreeMap<_, _>>();
775
776 output_notes_assets.append(&mut own_notes_assets);
778
779 let outgoing_assets = output_notes_assets.values().flat_map(|note_assets| note_assets.iter());
781
782 request::collect_assets(outgoing_assets)
783}
784
785fn validate_basic_account_request(
788 transaction_request: &TransactionRequest,
789 account: &Account,
790) -> Result<(), ClientError> {
791 let (fungible_balance_map, non_fungible_set) = get_outgoing_assets(transaction_request);
793
794 let (incoming_fungible_balance_map, incoming_non_fungible_balance_set) =
796 transaction_request.incoming_assets();
797
798 for (faucet_id, amount) in fungible_balance_map {
801 let account_asset_amount = account.vault().get_balance(faucet_id).unwrap_or(0);
802 let incoming_balance = incoming_fungible_balance_map.get(&faucet_id).unwrap_or(&0);
803 if account_asset_amount + incoming_balance < amount {
804 return Err(ClientError::AssetError(AssetError::FungibleAssetAmountNotSufficient {
805 minuend: account_asset_amount,
806 subtrahend: amount,
807 }));
808 }
809 }
810
811 for non_fungible in non_fungible_set {
814 match account.vault().has_non_fungible_asset(non_fungible) {
815 Ok(true) => (),
816 Ok(false) => {
817 if !incoming_non_fungible_balance_set.contains(&non_fungible) {
819 return Err(ClientError::AssetError(
820 AssetError::NonFungibleFaucetIdTypeMismatch(
821 non_fungible.faucet_id_prefix(),
822 ),
823 ));
824 }
825 },
826 _ => {
827 return Err(ClientError::AssetError(AssetError::NonFungibleFaucetIdTypeMismatch(
828 non_fungible.faucet_id_prefix(),
829 )));
830 },
831 }
832 }
833
834 Ok(())
835}
836
837pub fn notes_from_output(output_notes: &OutputNotes) -> impl Iterator<Item = &Note> {
842 output_notes.iter().filter_map(|n| match n {
843 OutputNote::Full(n) => Some(n),
844 OutputNote::Header(_) | OutputNote::Partial(_) => None,
845 })
846}
847
848fn validate_executed_transaction(
851 executed_transaction: &ExecutedTransaction,
852 expected_output_recipients: &[NoteRecipient],
853) -> Result<(), ClientError> {
854 let tx_output_recipient_digests = executed_transaction
855 .output_notes()
856 .iter()
857 .filter_map(|n| n.recipient().map(NoteRecipient::digest))
858 .collect::<Vec<_>>();
859
860 let missing_recipient_digest: Vec<Word> = expected_output_recipients
861 .iter()
862 .filter_map(|recipient| {
863 (!tx_output_recipient_digests.contains(&recipient.digest()))
864 .then_some(recipient.digest())
865 })
866 .collect();
867
868 if !missing_recipient_digest.is_empty() {
869 return Err(ClientError::MissingOutputRecipients(missing_recipient_digest));
870 }
871
872 Ok(())
873}