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::{NoteScreener, 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 let account_record = self.try_get_account(account_id).await?;
437
438 if account_record.is_locked() {
439 return Err(ClientError::AccountLocked(account_id));
440 }
441
442 self.store.apply_transaction(tx_update).await?;
443 info!("Transaction stored.");
444 Ok(())
445 }
446
447 pub async fn execute_program(
452 &mut self,
453 account_id: AccountId,
454 tx_script: TransactionScript,
455 advice_inputs: AdviceInputs,
456 foreign_accounts: BTreeSet<ForeignAccount>,
457 ) -> Result<[Felt; 16], ClientError> {
458 let (fpi_block_number, foreign_account_inputs) =
459 self.retrieve_foreign_account_inputs(foreign_accounts).await?;
460
461 let block_ref = if let Some(block_number) = fpi_block_number {
462 block_number
463 } else {
464 self.get_sync_height().await?
465 };
466
467 let account_record = self
468 .store
469 .get_account(account_id)
470 .await?
471 .ok_or(ClientError::AccountDataNotFound(account_id))?;
472
473 let account: Account = account_record.try_into()?;
474
475 let data_store = ClientDataStore::new(self.store.clone());
476
477 data_store.register_foreign_account_inputs(foreign_account_inputs.iter().cloned());
478
479 data_store.mast_store().load_account_code(account.code());
481
482 for fpi_account in &foreign_account_inputs {
483 data_store.mast_store().load_account_code(fpi_account.code());
484 }
485
486 Ok(self
487 .build_executor(&data_store)?
488 .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
489 .await?)
490 }
491
492 async fn get_note_updates(
505 &self,
506 submission_height: BlockNumber,
507 tx_result: &TransactionResult,
508 ) -> Result<NoteUpdateTracker, ClientError> {
509 let executed_tx = tx_result.executed_transaction();
510 let current_timestamp = self.store.get_current_timestamp();
511 let current_block_num = self.store.get_sync_height().await?;
512
513 let new_output_notes = executed_tx
515 .output_notes()
516 .iter()
517 .cloned()
518 .filter_map(|output_note| {
519 OutputNoteRecord::try_from_output_note(output_note, submission_height).ok()
520 })
521 .collect::<Vec<_>>();
522
523 let mut new_input_notes = vec![];
525 let note_screener = NoteScreener::new(self.store.clone(), self.authenticator.clone());
526
527 for note in notes_from_output(executed_tx.output_notes()) {
528 let account_relevance = note_screener.check_relevance(note).await?;
530 if !account_relevance.is_empty() {
531 let metadata = note.metadata().clone();
532
533 new_input_notes.push(InputNoteRecord::new(
534 note.into(),
535 current_timestamp,
536 ExpectedNoteState {
537 metadata: Some(metadata.clone()),
538 after_block_num: submission_height,
539 tag: Some(metadata.tag()),
540 }
541 .into(),
542 ));
543 }
544 }
545
546 new_input_notes.extend(tx_result.future_notes().iter().map(|(note_details, tag)| {
548 InputNoteRecord::new(
549 note_details.clone(),
550 None,
551 ExpectedNoteState {
552 metadata: None,
553 after_block_num: current_block_num,
554 tag: Some(*tag),
555 }
556 .into(),
557 )
558 }));
559
560 let consumed_note_ids =
562 executed_tx.tx_inputs().input_notes().iter().map(InputNote::id).collect();
563
564 let consumed_notes = self.get_input_notes(NoteFilter::List(consumed_note_ids)).await?;
565
566 let mut updated_input_notes = vec![];
567
568 for mut input_note_record in consumed_notes {
569 if input_note_record.consumed_locally(
570 executed_tx.account_id(),
571 executed_tx.id(),
572 self.store.get_current_timestamp(),
573 )? {
574 updated_input_notes.push(input_note_record);
575 }
576 }
577
578 Ok(NoteUpdateTracker::for_transaction_updates(
579 new_input_notes,
580 updated_input_notes,
581 new_output_notes,
582 ))
583 }
584
585 pub async fn validate_request(
592 &mut self,
593 account_id: AccountId,
594 transaction_request: &TransactionRequest,
595 ) -> Result<(), ClientError> {
596 if let Some(max_block_number_delta) = self.max_block_number_delta {
597 let current_chain_tip =
598 self.rpc_api.get_block_header_by_number(None, false).await?.0.block_num();
599
600 if current_chain_tip > self.store.get_sync_height().await? + max_block_number_delta {
601 return Err(ClientError::RecencyConditionError(
602 "The client is too far behind the chain tip to execute the transaction",
603 ));
604 }
605 }
606
607 let account: Account = self.try_get_account(account_id).await?.try_into()?;
608
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: Account = self.try_get_account(account_id).await?.try_into()?;
664
665 Ok(AccountInterface::from_account(&account))
666 }
667
668 async fn retrieve_foreign_account_inputs(
679 &mut self,
680 foreign_accounts: BTreeSet<ForeignAccount>,
681 ) -> Result<(Option<BlockNumber>, Vec<AccountInputs>), ClientError> {
682 if foreign_accounts.is_empty() {
683 return Ok((None, Vec::new()));
684 }
685
686 let block_num = self.get_sync_height().await?;
687 let mut return_foreign_account_inputs = Vec::with_capacity(foreign_accounts.len());
688
689 for foreign_account in foreign_accounts {
690 let account_id = foreign_account.account_id();
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(
701 foreign_account.clone(),
702 AccountStateAt::Block(block_num),
703 known_account_code,
704 )
705 .await?;
706 let foreign_account_inputs = match foreign_account {
707 ForeignAccount::Public(account_id, ..) => {
708 let foreign_account_inputs: AccountInputs =
709 account_proof_into_inputs(account_proof)?;
710
711 self.store
713 .upsert_foreign_account_code(
714 account_id,
715 foreign_account_inputs.code().clone(),
716 )
717 .await?;
718
719 foreign_account_inputs
720 },
721 ForeignAccount::Private(partial_account) => {
722 let (witness, _) = account_proof.into_parts();
723
724 AccountInputs::new(partial_account.clone(), witness)
725 },
726 };
727
728 return_foreign_account_inputs.push(foreign_account_inputs);
729 }
730
731 Ok((Some(block_num), return_foreign_account_inputs))
732 }
733
734 pub(crate) fn build_executor<'store, 'auth, STORE: DataStore + Sync>(
737 &'auth self,
738 data_store: &'store STORE,
739 ) -> Result<TransactionExecutor<'store, 'auth, STORE, AUTH>, TransactionExecutorError> {
740 let mut executor = TransactionExecutor::new(data_store).with_options(self.exec_options)?;
741 if let Some(authenticator) = self.authenticator.as_deref() {
742 executor = executor.with_authenticator(authenticator);
743 }
744 executor = executor.with_source_manager(self.source_manager.clone());
745
746 Ok(executor)
747 }
748}
749
750fn get_outgoing_assets(
758 transaction_request: &TransactionRequest,
759) -> (BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>) {
760 let mut own_notes_assets = match transaction_request.script_template() {
762 Some(TransactionScriptTemplate::SendNotes(notes)) => notes
763 .iter()
764 .map(|note| (note.id(), note.assets().clone()))
765 .collect::<BTreeMap<_, _>>(),
766 _ => BTreeMap::default(),
767 };
768 let mut output_notes_assets = transaction_request
770 .expected_output_own_notes()
771 .into_iter()
772 .map(|note| (note.id(), note.assets().clone()))
773 .collect::<BTreeMap<_, _>>();
774
775 output_notes_assets.append(&mut own_notes_assets);
777
778 let outgoing_assets = output_notes_assets.values().flat_map(|note_assets| note_assets.iter());
780
781 request::collect_assets(outgoing_assets)
782}
783
784fn validate_basic_account_request(
787 transaction_request: &TransactionRequest,
788 account: &Account,
789) -> Result<(), ClientError> {
790 let (fungible_balance_map, non_fungible_set) = get_outgoing_assets(transaction_request);
792
793 let (incoming_fungible_balance_map, incoming_non_fungible_balance_set) =
795 transaction_request.incoming_assets();
796
797 for (faucet_id, amount) in fungible_balance_map {
800 let account_asset_amount = account.vault().get_balance(faucet_id).unwrap_or(0);
801 let incoming_balance = incoming_fungible_balance_map.get(&faucet_id).unwrap_or(&0);
802 if account_asset_amount + incoming_balance < amount {
803 return Err(ClientError::AssetError(AssetError::FungibleAssetAmountNotSufficient {
804 minuend: account_asset_amount,
805 subtrahend: amount,
806 }));
807 }
808 }
809
810 for non_fungible in non_fungible_set {
813 match account.vault().has_non_fungible_asset(non_fungible) {
814 Ok(true) => (),
815 Ok(false) => {
816 if !incoming_non_fungible_balance_set.contains(&non_fungible) {
818 return Err(ClientError::AssetError(
819 AssetError::NonFungibleFaucetIdTypeMismatch(
820 non_fungible.faucet_id_prefix(),
821 ),
822 ));
823 }
824 },
825 _ => {
826 return Err(ClientError::AssetError(AssetError::NonFungibleFaucetIdTypeMismatch(
827 non_fungible.faucet_id_prefix(),
828 )));
829 },
830 }
831 }
832
833 Ok(())
834}
835
836pub fn notes_from_output(output_notes: &OutputNotes) -> impl Iterator<Item = &Note> {
841 output_notes
842 .iter()
843 .filter(|n| matches!(n, OutputNote::Full(_)))
844 .map(|n| match n {
845 OutputNote::Full(n) => n,
846 OutputNote::Header(_) | OutputNote::Partial(_) => {
849 todo!("For now, all details should be held in OutputNote::Fulls")
850 },
851 })
852}
853
854fn validate_executed_transaction(
857 executed_transaction: &ExecutedTransaction,
858 expected_output_recipients: &[NoteRecipient],
859) -> Result<(), ClientError> {
860 let tx_output_recipient_digests = executed_transaction
861 .output_notes()
862 .iter()
863 .filter_map(|n| n.recipient().map(NoteRecipient::digest))
864 .collect::<Vec<_>>();
865
866 let missing_recipient_digest: Vec<Word> = expected_output_recipients
867 .iter()
868 .filter_map(|recipient| {
869 (!tx_output_recipient_digests.contains(&recipient.digest()))
870 .then_some(recipient.digest())
871 })
872 .collect();
873
874 if !missing_recipient_digest.is_empty() {
875 return Err(ClientError::MissingOutputRecipients(missing_recipient_digest));
876 }
877
878 Ok(())
879}