1use alloc::boxed::Box;
73use alloc::collections::{BTreeMap, BTreeSet};
74use alloc::string::ToString;
75use alloc::sync::Arc;
76use alloc::vec::Vec;
77use core::fmt::{self};
78
79use miden_objects::account::{Account, AccountCode, AccountDelta, AccountId};
80use miden_objects::asset::{Asset, NonFungibleAsset};
81use miden_objects::block::BlockNumber;
82use miden_objects::note::{Note, NoteDetails, NoteId, NoteRecipient, NoteTag};
83use miden_objects::transaction::{AccountInputs, TransactionArgs};
84use miden_objects::{AssetError, Felt, Word};
85use miden_remote_prover_client::remote_prover::tx_prover::RemoteTransactionProver;
86use miden_tx::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable};
87use miden_tx::{DataStore, NoteConsumptionChecker, TransactionExecutor};
88use tracing::info;
89
90use super::Client;
91use crate::ClientError;
92use crate::note::{NoteScreener, NoteUpdateTracker};
93use crate::rpc::domain::account::AccountProof;
94use crate::store::data_store::ClientDataStore;
95use crate::store::input_note_states::ExpectedNoteState;
96use crate::store::{
97 InputNoteRecord,
98 InputNoteState,
99 NoteFilter,
100 OutputNoteRecord,
101 StoreError,
102 TransactionFilter,
103};
104use crate::sync::NoteTagRecord;
105
106mod request;
107
108pub use miden_lib::account::interface::{AccountComponentInterface, AccountInterface};
112pub use miden_lib::transaction::TransactionKernel;
113pub use miden_objects::transaction::{
114 ExecutedTransaction,
115 InputNote,
116 InputNotes,
117 OutputNote,
118 OutputNotes,
119 ProvenTransaction,
120 TransactionId,
121 TransactionScript,
122 TransactionWitness,
123};
124pub use miden_objects::vm::{AdviceInputs, AdviceMap};
125pub use miden_tx::auth::TransactionAuthenticator;
126pub use miden_tx::{
127 DataStoreError,
128 LocalTransactionProver,
129 ProvingOptions,
130 TransactionExecutorError,
131 TransactionProverError,
132};
133pub use request::{
134 ForeignAccount,
135 NoteArgs,
136 PaymentNoteDescription,
137 SwapTransactionData,
138 TransactionRequest,
139 TransactionRequestBuilder,
140 TransactionRequestError,
141 TransactionScriptTemplate,
142};
143
144#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
148#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
149pub trait TransactionProver {
150 async fn prove(
151 &self,
152 tx_result: TransactionWitness,
153 ) -> Result<ProvenTransaction, TransactionProverError>;
154}
155
156#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
157#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
158impl TransactionProver for LocalTransactionProver {
159 async fn prove(
160 &self,
161 witness: TransactionWitness,
162 ) -> Result<ProvenTransaction, TransactionProverError> {
163 LocalTransactionProver::prove(self, witness)
164 }
165}
166
167#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
168#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
169impl TransactionProver for RemoteTransactionProver {
170 async fn prove(
171 &self,
172 witness: TransactionWitness,
173 ) -> Result<ProvenTransaction, TransactionProverError> {
174 let fut = RemoteTransactionProver::prove(self, witness);
175 fut.await
176 }
177}
178
179#[derive(Clone, Debug, PartialEq)]
184pub struct TransactionResult {
185 transaction: ExecutedTransaction,
186 future_notes: Vec<(NoteDetails, NoteTag)>,
187}
188
189impl TransactionResult {
190 pub fn new(
193 transaction: ExecutedTransaction,
194 future_notes: Vec<(NoteDetails, NoteTag)>,
195 ) -> Result<Self, ClientError> {
196 Ok(Self { transaction, future_notes })
197 }
198
199 pub fn executed_transaction(&self) -> &ExecutedTransaction {
201 &self.transaction
202 }
203
204 pub fn created_notes(&self) -> &OutputNotes {
206 self.transaction.output_notes()
207 }
208
209 pub fn future_notes(&self) -> &[(NoteDetails, NoteTag)] {
212 &self.future_notes
213 }
214
215 pub fn block_num(&self) -> BlockNumber {
217 self.transaction.block_header().block_num()
218 }
219
220 pub fn transaction_arguments(&self) -> &TransactionArgs {
222 self.transaction.tx_args()
223 }
224
225 pub fn account_delta(&self) -> &AccountDelta {
227 self.transaction.account_delta()
228 }
229
230 pub fn consumed_notes(&self) -> &InputNotes<InputNote> {
232 self.transaction.tx_inputs().input_notes()
233 }
234}
235
236impl From<TransactionResult> for ExecutedTransaction {
237 fn from(tx_result: TransactionResult) -> ExecutedTransaction {
238 tx_result.transaction
239 }
240}
241
242impl Serializable for TransactionResult {
243 fn write_into<W: ByteWriter>(&self, target: &mut W) {
244 self.transaction.write_into(target);
245 self.future_notes.write_into(target);
246 }
247}
248
249impl Deserializable for TransactionResult {
250 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
251 let transaction = ExecutedTransaction::read_from(source)?;
252 let future_notes = Vec::<(NoteDetails, NoteTag)>::read_from(source)?;
253
254 Ok(Self { transaction, future_notes })
255 }
256}
257
258#[derive(Debug, Clone)]
263pub struct TransactionRecord {
264 pub id: TransactionId,
266 pub details: TransactionDetails,
268 pub script: Option<TransactionScript>,
271 pub status: TransactionStatus,
273}
274
275impl TransactionRecord {
276 pub fn new(
278 id: TransactionId,
279 details: TransactionDetails,
280 script: Option<TransactionScript>,
281 status: TransactionStatus,
282 ) -> TransactionRecord {
283 TransactionRecord { id, details, script, status }
284 }
285
286 pub fn commit_transaction(
289 &mut self,
290 commit_height: BlockNumber,
291 commit_timestamp: u64,
292 ) -> bool {
293 match self.status {
294 TransactionStatus::Pending => {
295 self.status = TransactionStatus::Committed {
296 block_number: commit_height,
297 commit_timestamp,
298 };
299 true
300 },
301 TransactionStatus::Discarded(_) | TransactionStatus::Committed { .. } => false,
302 }
303 }
304
305 pub fn discard_transaction(&mut self, cause: DiscardCause) -> bool {
308 match self.status {
309 TransactionStatus::Pending => {
310 self.status = TransactionStatus::Discarded(cause);
311 true
312 },
313 TransactionStatus::Discarded(_) | TransactionStatus::Committed { .. } => false,
314 }
315 }
316}
317
318#[derive(Debug, Clone)]
320pub struct TransactionDetails {
321 pub account_id: AccountId,
323 pub init_account_state: Word,
325 pub final_account_state: Word,
327 pub input_note_nullifiers: Vec<Word>,
329 pub output_notes: OutputNotes,
331 pub block_num: BlockNumber,
333 pub submission_height: BlockNumber,
335 pub expiration_block_num: BlockNumber,
337 pub creation_timestamp: u64,
339}
340
341impl Serializable for TransactionDetails {
342 fn write_into<W: ByteWriter>(&self, target: &mut W) {
343 self.account_id.write_into(target);
344 self.init_account_state.write_into(target);
345 self.final_account_state.write_into(target);
346 self.input_note_nullifiers.write_into(target);
347 self.output_notes.write_into(target);
348 self.block_num.write_into(target);
349 self.submission_height.write_into(target);
350 self.expiration_block_num.write_into(target);
351 self.creation_timestamp.write_into(target);
352 }
353}
354
355impl Deserializable for TransactionDetails {
356 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
357 let account_id = AccountId::read_from(source)?;
358 let init_account_state = Word::read_from(source)?;
359 let final_account_state = Word::read_from(source)?;
360 let input_note_nullifiers = Vec::<Word>::read_from(source)?;
361 let output_notes = OutputNotes::read_from(source)?;
362 let block_num = BlockNumber::read_from(source)?;
363 let submission_height = BlockNumber::read_from(source)?;
364 let expiration_block_num = BlockNumber::read_from(source)?;
365 let creation_timestamp = source.read_u64()?;
366
367 Ok(Self {
368 account_id,
369 init_account_state,
370 final_account_state,
371 input_note_nullifiers,
372 output_notes,
373 block_num,
374 submission_height,
375 expiration_block_num,
376 creation_timestamp,
377 })
378 }
379}
380
381#[derive(Debug, Clone, Copy, PartialEq)]
383pub enum DiscardCause {
384 Expired,
385 InputConsumed,
386 DiscardedInitialState,
387 Stale,
388}
389
390impl DiscardCause {
391 pub fn from_string(cause: &str) -> Result<Self, DeserializationError> {
392 match cause {
393 "Expired" => Ok(DiscardCause::Expired),
394 "InputConsumed" => Ok(DiscardCause::InputConsumed),
395 "DiscardedInitialState" => Ok(DiscardCause::DiscardedInitialState),
396 "Stale" => Ok(DiscardCause::Stale),
397 _ => Err(DeserializationError::InvalidValue(format!("Invalid discard cause: {cause}"))),
398 }
399 }
400}
401
402impl fmt::Display for DiscardCause {
403 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
404 match self {
405 DiscardCause::Expired => write!(f, "Expired"),
406 DiscardCause::InputConsumed => write!(f, "InputConsumed"),
407 DiscardCause::DiscardedInitialState => write!(f, "DiscardedInitialState"),
408 DiscardCause::Stale => write!(f, "Stale"),
409 }
410 }
411}
412
413impl Serializable for DiscardCause {
414 fn write_into<W: ByteWriter>(&self, target: &mut W) {
415 match self {
416 DiscardCause::Expired => target.write_u8(0),
417 DiscardCause::InputConsumed => target.write_u8(1),
418 DiscardCause::DiscardedInitialState => target.write_u8(2),
419 DiscardCause::Stale => target.write_u8(3),
420 }
421 }
422}
423
424impl Deserializable for DiscardCause {
425 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
426 match source.read_u8()? {
427 0 => Ok(DiscardCause::Expired),
428 1 => Ok(DiscardCause::InputConsumed),
429 2 => Ok(DiscardCause::DiscardedInitialState),
430 3 => Ok(DiscardCause::Stale),
431 _ => Err(DeserializationError::InvalidValue("Invalid discard cause".to_string())),
432 }
433 }
434}
435
436#[derive(Debug, Clone, PartialEq)]
438pub enum TransactionStatus {
439 Pending,
441 Committed {
443 block_number: BlockNumber,
445 commit_timestamp: u64,
447 },
448 Discarded(DiscardCause),
450}
451
452pub enum TransactionStatusVariant {
453 Pending = 0,
454 Committed = 1,
455 Discarded = 2,
456}
457
458impl TransactionStatus {
459 pub const fn variant(&self) -> TransactionStatusVariant {
460 match self {
461 TransactionStatus::Pending => TransactionStatusVariant::Pending,
462 TransactionStatus::Committed { .. } => TransactionStatusVariant::Committed,
463 TransactionStatus::Discarded(_) => TransactionStatusVariant::Discarded,
464 }
465 }
466}
467
468impl fmt::Display for TransactionStatus {
469 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
470 match self {
471 TransactionStatus::Pending => write!(f, "Pending"),
472 TransactionStatus::Committed { block_number, .. } => {
473 write!(f, "Committed (Block: {block_number})")
474 },
475 TransactionStatus::Discarded(cause) => write!(f, "Discarded ({cause})",),
476 }
477 }
478}
479
480impl Serializable for TransactionStatus {
481 fn write_into<W: ByteWriter>(&self, target: &mut W) {
482 match self {
483 TransactionStatus::Pending => target.write_u8(self.variant() as u8),
484 TransactionStatus::Committed { block_number, commit_timestamp } => {
485 target.write_u8(self.variant() as u8);
486 block_number.write_into(target);
487 commit_timestamp.write_into(target);
488 },
489 TransactionStatus::Discarded(cause) => {
490 target.write_u8(self.variant() as u8);
491 cause.write_into(target);
492 },
493 }
494 }
495}
496
497impl Deserializable for TransactionStatus {
498 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
499 match source.read_u8()? {
500 variant if variant == TransactionStatusVariant::Pending as u8 => {
501 Ok(TransactionStatus::Pending)
502 },
503 variant if variant == TransactionStatusVariant::Committed as u8 => {
504 let block_number = BlockNumber::read_from(source)?;
505 let commit_timestamp = source.read_u64()?;
506 Ok(TransactionStatus::Committed { block_number, commit_timestamp })
507 },
508 variant if variant == TransactionStatusVariant::Discarded as u8 => {
509 let cause = DiscardCause::read_from(source)?;
510 Ok(TransactionStatus::Discarded(cause))
511 },
512 _ => Err(DeserializationError::InvalidValue("Invalid transaction status".to_string())),
513 }
514 }
515}
516
517pub struct TransactionStoreUpdate {
523 executed_transaction: ExecutedTransaction,
525 submission_height: BlockNumber,
527 note_updates: NoteUpdateTracker,
529 new_tags: Vec<NoteTagRecord>,
531}
532
533impl TransactionStoreUpdate {
534 pub fn new(
543 executed_transaction: ExecutedTransaction,
544 submission_height: BlockNumber,
545 note_updates: NoteUpdateTracker,
546 new_tags: Vec<NoteTagRecord>,
547 ) -> Self {
548 Self {
549 executed_transaction,
550 submission_height,
551 note_updates,
552 new_tags,
553 }
554 }
555
556 pub fn executed_transaction(&self) -> &ExecutedTransaction {
558 &self.executed_transaction
559 }
560
561 pub fn submission_height(&self) -> BlockNumber {
563 self.submission_height
564 }
565
566 pub fn note_updates(&self) -> &NoteUpdateTracker {
568 &self.note_updates
569 }
570
571 pub fn new_tags(&self) -> &[NoteTagRecord] {
573 &self.new_tags
574 }
575}
576
577impl<AUTH> Client<AUTH>
579where
580 AUTH: TransactionAuthenticator + Sync + 'static,
581{
582 pub async fn get_transactions(
587 &self,
588 filter: TransactionFilter,
589 ) -> Result<Vec<TransactionRecord>, ClientError> {
590 self.store.get_transactions(filter).await.map_err(Into::into)
591 }
592
593 pub async fn new_transaction(
610 &mut self,
611 account_id: AccountId,
612 transaction_request: TransactionRequest,
613 ) -> Result<TransactionResult, ClientError> {
614 self.validate_request(account_id, &transaction_request).await?;
616
617 let authenticated_input_note_ids: Vec<NoteId> =
620 transaction_request.authenticated_input_note_ids().collect::<Vec<_>>();
621
622 let authenticated_note_records = self
623 .store
624 .get_input_notes(NoteFilter::List(authenticated_input_note_ids))
625 .await?;
626
627 let unauthenticated_input_notes = transaction_request
629 .unauthenticated_input_notes()
630 .iter()
631 .cloned()
632 .map(Into::into)
633 .collect::<Vec<_>>();
634
635 self.store.upsert_input_notes(&unauthenticated_input_notes).await?;
636
637 let mut notes = transaction_request.build_input_notes(authenticated_note_records)?;
638
639 let output_recipients =
640 transaction_request.expected_output_recipients().cloned().collect::<Vec<_>>();
641
642 let future_notes: Vec<(NoteDetails, NoteTag)> =
643 transaction_request.expected_future_notes().cloned().collect();
644
645 let tx_script = transaction_request.build_transaction_script(
646 &self.get_account_interface(account_id).await?,
647 self.in_debug_mode().into(),
648 )?;
649
650 let foreign_accounts = transaction_request.foreign_accounts().clone();
651
652 let (fpi_block_num, foreign_account_inputs) =
654 self.retrieve_foreign_account_inputs(foreign_accounts).await?;
655
656 let ignore_invalid_notes = transaction_request.ignore_invalid_input_notes();
657
658 let data_store = ClientDataStore::new(self.store.clone());
659 for fpi_account in &foreign_account_inputs {
660 data_store.mast_store().load_account_code(fpi_account.code());
661 }
662
663 let tx_args = transaction_request.into_transaction_args(tx_script, foreign_account_inputs);
664
665 let block_num = if let Some(block_num) = fpi_block_num {
666 block_num
667 } else {
668 self.store.get_sync_height().await?
669 };
670
671 let account_record = self
673 .store
674 .get_account(account_id)
675 .await?
676 .ok_or(ClientError::AccountDataNotFound(account_id))?;
677 let account: Account = account_record.into();
678 data_store.mast_store().load_account_code(account.code());
679
680 if ignore_invalid_notes {
681 notes = self.get_valid_input_notes(account, notes, tx_args.clone()).await?;
683 }
684
685 let executed_transaction = self
687 .build_executor(&data_store)?
688 .execute_transaction(account_id, block_num, notes, tx_args)
689 .await?;
690
691 validate_executed_transaction(&executed_transaction, &output_recipients)?;
692
693 TransactionResult::new(executed_transaction, future_notes)
694 }
695
696 pub async fn submit_transaction(
699 &mut self,
700 tx_result: TransactionResult,
701 ) -> Result<(), ClientError> {
702 self.submit_transaction_with_prover(tx_result, self.tx_prover.clone()).await
703 }
704
705 pub async fn submit_transaction_with_prover(
708 &mut self,
709 tx_result: TransactionResult,
710 tx_prover: Arc<dyn TransactionProver>,
711 ) -> Result<(), ClientError> {
712 let proven_transaction = self.prove_transaction(&tx_result, tx_prover).await?;
713 let block_num = self.submit_proven_transaction(proven_transaction).await?;
714 self.apply_transaction(block_num, tx_result).await
715 }
716
717 async fn prove_transaction(
719 &mut self,
720 tx_result: &TransactionResult,
721 tx_prover: Arc<dyn TransactionProver>,
722 ) -> Result<ProvenTransaction, ClientError> {
723 info!("Proving transaction...");
724
725 let proven_transaction =
726 tx_prover.prove(tx_result.executed_transaction().clone().into()).await?;
727
728 info!("Transaction proven.");
729
730 Ok(proven_transaction)
731 }
732
733 async fn submit_proven_transaction(
734 &mut self,
735 proven_transaction: ProvenTransaction,
736 ) -> Result<BlockNumber, ClientError> {
737 info!("Submitting transaction to the network...");
738 let block_num = self.rpc_api.submit_proven_transaction(proven_transaction).await?;
739 info!("Transaction submitted.");
740
741 Ok(block_num)
742 }
743
744 async fn apply_transaction(
745 &self,
746 submission_height: BlockNumber,
747 tx_result: TransactionResult,
748 ) -> Result<(), ClientError> {
749 info!("Applying transaction to the local store...");
752
753 let account_id = tx_result.executed_transaction().account_id();
754 let account_record = self.try_get_account(account_id).await?;
755
756 if account_record.is_locked() {
757 return Err(ClientError::AccountLocked(account_id));
758 }
759
760 let final_commitment = tx_result.executed_transaction().final_account().commitment();
761 if self.store.get_account_header_by_commitment(final_commitment).await?.is_some() {
762 return Err(ClientError::StoreError(StoreError::AccountCommitmentAlreadyExists(
763 final_commitment,
764 )));
765 }
766
767 let note_updates = self.get_note_updates(submission_height, &tx_result).await?;
768
769 let new_tags = note_updates
770 .updated_input_notes()
771 .filter_map(|note| {
772 let note = note.inner();
773
774 if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) =
775 note.state()
776 {
777 Some(NoteTagRecord::with_note_source(*tag, note.id()))
778 } else {
779 None
780 }
781 })
782 .collect();
783
784 let tx_update = TransactionStoreUpdate::new(
785 tx_result.into(),
786 submission_height,
787 note_updates,
788 new_tags,
789 );
790
791 self.store.apply_transaction(tx_update).await?;
792 info!("Transaction stored.");
793 Ok(())
794 }
795
796 pub async fn execute_program(
801 &mut self,
802 account_id: AccountId,
803 tx_script: TransactionScript,
804 advice_inputs: AdviceInputs,
805 foreign_accounts: BTreeSet<ForeignAccount>,
806 ) -> Result<[Felt; 16], ClientError> {
807 let (fpi_block_number, foreign_account_inputs) =
808 self.retrieve_foreign_account_inputs(foreign_accounts).await?;
809
810 let block_ref = if let Some(block_number) = fpi_block_number {
811 block_number
812 } else {
813 self.get_sync_height().await?
814 };
815
816 let account_record = self
817 .store
818 .get_account(account_id)
819 .await?
820 .ok_or(ClientError::AccountDataNotFound(account_id))?;
821
822 let account: Account = account_record.into();
823
824 let data_store = ClientDataStore::new(self.store.clone());
825
826 data_store.mast_store().load_account_code(account.code());
828
829 for fpi_account in &foreign_account_inputs {
830 data_store.mast_store().load_account_code(fpi_account.code());
831 }
832
833 Ok(self
834 .build_executor(&data_store)?
835 .execute_tx_view_script(
836 account_id,
837 block_ref,
838 tx_script,
839 advice_inputs,
840 foreign_account_inputs,
841 )
842 .await?)
843 }
844
845 async fn get_note_updates(
858 &self,
859 submission_height: BlockNumber,
860 tx_result: &TransactionResult,
861 ) -> Result<NoteUpdateTracker, ClientError> {
862 let executed_tx = tx_result.executed_transaction();
863 let current_timestamp = self.store.get_current_timestamp();
864 let current_block_num = self.store.get_sync_height().await?;
865
866 let new_output_notes = executed_tx
868 .output_notes()
869 .iter()
870 .cloned()
871 .filter_map(|output_note| {
872 OutputNoteRecord::try_from_output_note(output_note, submission_height).ok()
873 })
874 .collect::<Vec<_>>();
875
876 let mut new_input_notes = vec![];
878 let note_screener = NoteScreener::new(self.store.clone(), self.authenticator.clone());
879
880 for note in notes_from_output(executed_tx.output_notes()) {
881 let account_relevance = note_screener.check_relevance(note).await?;
883 if !account_relevance.is_empty() {
884 let metadata = *note.metadata();
885
886 new_input_notes.push(InputNoteRecord::new(
887 note.into(),
888 current_timestamp,
889 ExpectedNoteState {
890 metadata: Some(metadata),
891 after_block_num: submission_height,
892 tag: Some(metadata.tag()),
893 }
894 .into(),
895 ));
896 }
897 }
898
899 new_input_notes.extend(tx_result.future_notes().iter().map(|(note_details, tag)| {
901 InputNoteRecord::new(
902 note_details.clone(),
903 None,
904 ExpectedNoteState {
905 metadata: None,
906 after_block_num: current_block_num,
907 tag: Some(*tag),
908 }
909 .into(),
910 )
911 }));
912
913 let consumed_note_ids =
915 executed_tx.tx_inputs().input_notes().iter().map(InputNote::id).collect();
916
917 let consumed_notes = self.get_input_notes(NoteFilter::List(consumed_note_ids)).await?;
918
919 let mut updated_input_notes = vec![];
920
921 for mut input_note_record in consumed_notes {
922 if input_note_record.consumed_locally(
923 executed_tx.account_id(),
924 executed_tx.id(),
925 self.store.get_current_timestamp(),
926 )? {
927 updated_input_notes.push(input_note_record);
928 }
929 }
930
931 Ok(NoteUpdateTracker::for_transaction_updates(
932 new_input_notes,
933 updated_input_notes,
934 new_output_notes,
935 ))
936 }
937
938 fn get_outgoing_assets(
943 transaction_request: &TransactionRequest,
944 ) -> (BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>) {
945 let mut own_notes_assets = match transaction_request.script_template() {
947 Some(TransactionScriptTemplate::SendNotes(notes)) => notes
948 .iter()
949 .map(|note| (note.id(), note.assets().clone()))
950 .collect::<BTreeMap<_, _>>(),
951 _ => BTreeMap::default(),
952 };
953 let mut output_notes_assets = transaction_request
955 .expected_output_own_notes()
956 .into_iter()
957 .map(|note| (note.id(), note.assets().clone()))
958 .collect::<BTreeMap<_, _>>();
959
960 output_notes_assets.append(&mut own_notes_assets);
962
963 let outgoing_assets =
965 output_notes_assets.values().flat_map(|note_assets| note_assets.iter());
966
967 collect_assets(outgoing_assets)
968 }
969
970 async fn get_incoming_assets(
972 &self,
973 transaction_request: &TransactionRequest,
974 ) -> Result<(BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>), TransactionRequestError>
975 {
976 let incoming_notes_ids: Vec<_> = transaction_request
978 .input_notes()
979 .iter()
980 .filter_map(|(note_id, _)| {
981 if transaction_request
982 .unauthenticated_input_notes()
983 .iter()
984 .any(|note| note.id() == *note_id)
985 {
986 None
987 } else {
988 Some(*note_id)
989 }
990 })
991 .collect();
992
993 let store_input_notes = self
994 .get_input_notes(NoteFilter::List(incoming_notes_ids))
995 .await
996 .map_err(|err| TransactionRequestError::NoteNotFound(err.to_string()))?;
997
998 let all_incoming_assets =
999 store_input_notes.iter().flat_map(|note| note.assets().iter()).chain(
1000 transaction_request
1001 .unauthenticated_input_notes()
1002 .iter()
1003 .flat_map(|note| note.assets().iter()),
1004 );
1005
1006 Ok(collect_assets(all_incoming_assets))
1007 }
1008
1009 async fn validate_basic_account_request(
1010 &self,
1011 transaction_request: &TransactionRequest,
1012 account: &Account,
1013 ) -> Result<(), ClientError> {
1014 let (fungible_balance_map, non_fungible_set) =
1016 Client::<AUTH>::get_outgoing_assets(transaction_request);
1017
1018 let (incoming_fungible_balance_map, incoming_non_fungible_balance_set) =
1020 self.get_incoming_assets(transaction_request).await?;
1021
1022 for (faucet_id, amount) in fungible_balance_map {
1025 let account_asset_amount = account.vault().get_balance(faucet_id).unwrap_or(0);
1026 let incoming_balance = incoming_fungible_balance_map.get(&faucet_id).unwrap_or(&0);
1027 if account_asset_amount + incoming_balance < amount {
1028 return Err(ClientError::AssetError(
1029 AssetError::FungibleAssetAmountNotSufficient {
1030 minuend: account_asset_amount,
1031 subtrahend: amount,
1032 },
1033 ));
1034 }
1035 }
1036
1037 for non_fungible in non_fungible_set {
1040 match account.vault().has_non_fungible_asset(non_fungible) {
1041 Ok(true) => (),
1042 Ok(false) => {
1043 if !incoming_non_fungible_balance_set.contains(&non_fungible) {
1045 return Err(ClientError::AssetError(
1046 AssetError::NonFungibleFaucetIdTypeMismatch(
1047 non_fungible.faucet_id_prefix(),
1048 ),
1049 ));
1050 }
1051 },
1052 _ => {
1053 return Err(ClientError::AssetError(
1054 AssetError::NonFungibleFaucetIdTypeMismatch(
1055 non_fungible.faucet_id_prefix(),
1056 ),
1057 ));
1058 },
1059 }
1060 }
1061
1062 Ok(())
1063 }
1064
1065 pub async fn validate_request(
1072 &mut self,
1073 account_id: AccountId,
1074 transaction_request: &TransactionRequest,
1075 ) -> Result<(), ClientError> {
1076 if let Some(max_block_number_delta) = self.max_block_number_delta {
1077 let current_chain_tip =
1078 self.rpc_api.get_block_header_by_number(None, false).await?.0.block_num();
1079
1080 if current_chain_tip > self.store.get_sync_height().await? + max_block_number_delta {
1081 return Err(ClientError::RecencyConditionError(
1082 "The client is too far behind the chain tip to execute the transaction"
1083 .to_string(),
1084 ));
1085 }
1086 }
1087
1088 let account: Account = self.try_get_account(account_id).await?.into();
1089
1090 if account.is_faucet() {
1091 Ok(())
1093 } else {
1094 self.validate_basic_account_request(transaction_request, &account).await
1095 }
1096 }
1097
1098 async fn get_valid_input_notes(
1099 &self,
1100 account: Account,
1101 mut input_notes: InputNotes<InputNote>,
1102 tx_args: TransactionArgs,
1103 ) -> Result<InputNotes<InputNote>, ClientError> {
1104 loop {
1105 let data_store = ClientDataStore::new(self.store.clone());
1106
1107 data_store.mast_store().load_account_code(account.code());
1108 let execution = NoteConsumptionChecker::new(&self.build_executor(&data_store)?)
1109 .check_notes_consumability(
1110 account.id(),
1111 self.store.get_sync_height().await?,
1112 input_notes.clone(),
1113 tx_args.clone(),
1114 )
1115 .await?;
1116
1117 if execution.failed.is_empty() {
1118 break;
1119 }
1120
1121 let failed_note_ids: BTreeSet<NoteId> =
1122 execution.failed.iter().map(|n| n.note.id()).collect();
1123 let filtered_input_notes = InputNotes::new(
1124 input_notes
1125 .into_iter()
1126 .filter(|note| !failed_note_ids.contains(¬e.id()))
1127 .collect(),
1128 )
1129 .expect("Created from a valid input notes list");
1130
1131 input_notes = filtered_input_notes;
1132 }
1133
1134 Ok(input_notes)
1135 }
1136
1137 pub(crate) async fn get_account_interface(
1139 &self,
1140 account_id: AccountId,
1141 ) -> Result<AccountInterface, ClientError> {
1142 let account: Account = self.try_get_account(account_id).await?.into();
1143
1144 Ok(AccountInterface::from(&account))
1145 }
1146
1147 async fn retrieve_foreign_account_inputs(
1158 &mut self,
1159 foreign_accounts: BTreeSet<ForeignAccount>,
1160 ) -> Result<(Option<BlockNumber>, Vec<AccountInputs>), ClientError> {
1161 if foreign_accounts.is_empty() {
1162 return Ok((None, Vec::new()));
1163 }
1164
1165 let mut return_foreign_account_inputs = Vec::with_capacity(foreign_accounts.len());
1166
1167 let account_ids = foreign_accounts.iter().map(ForeignAccount::account_id);
1168 let known_account_codes =
1169 self.store.get_foreign_account_code(account_ids.collect()).await?;
1170
1171 let known_account_codes: Vec<AccountCode> = known_account_codes.into_values().collect();
1172
1173 let (block_num, account_proofs) =
1175 self.rpc_api.get_account_proofs(&foreign_accounts, known_account_codes).await?;
1176
1177 let mut account_proofs: BTreeMap<AccountId, AccountProof> =
1178 account_proofs.into_iter().map(|proof| (proof.account_id(), proof)).collect();
1179
1180 for foreign_account in &foreign_accounts {
1181 let foreign_account_inputs = match foreign_account {
1182 ForeignAccount::Public(account_id, ..) => {
1183 let account_proof = account_proofs
1184 .remove(account_id)
1185 .expect("proof was requested and received");
1186
1187 let foreign_account_inputs: AccountInputs = account_proof.try_into()?;
1188
1189 self.store
1191 .upsert_foreign_account_code(
1192 *account_id,
1193 foreign_account_inputs.code().clone(),
1194 )
1195 .await?;
1196
1197 foreign_account_inputs
1198 },
1199 ForeignAccount::Private(partial_account) => {
1200 let account_id = partial_account.id();
1201 let (witness, _) = account_proofs
1202 .remove(&account_id)
1203 .expect("proof was requested and received")
1204 .into_parts();
1205
1206 AccountInputs::new(partial_account.clone(), witness)
1207 },
1208 };
1209
1210 return_foreign_account_inputs.push(foreign_account_inputs);
1211 }
1212
1213 if self.store.get_block_header_by_num(block_num).await?.is_none() {
1215 info!(
1216 "Getting current block header data to execute transaction with foreign account requirements"
1217 );
1218 let summary = self.sync_state().await?;
1219
1220 if summary.block_num != block_num {
1221 let mut current_partial_mmr = self.build_current_partial_mmr().await?;
1222 self.get_and_store_authenticated_block(block_num, &mut current_partial_mmr)
1223 .await?;
1224 }
1225 }
1226
1227 Ok((Some(block_num), return_foreign_account_inputs))
1228 }
1229
1230 pub(crate) fn build_executor<'store, 'auth, STORE: DataStore + Sync>(
1231 &'auth self,
1232 data_store: &'store STORE,
1233 ) -> Result<TransactionExecutor<'store, 'auth, STORE, AUTH>, TransactionExecutorError> {
1234 let mut executor = TransactionExecutor::new(data_store).with_options(self.exec_options)?;
1235 if let Some(authenticator) = self.authenticator.as_deref() {
1236 executor = executor.with_authenticator(authenticator);
1237 }
1238
1239 Ok(executor)
1240 }
1241}
1242
1243#[cfg(feature = "testing")]
1247impl<AUTH: TransactionAuthenticator + Sync + 'static> Client<AUTH> {
1248 pub async fn testing_prove_transaction(
1249 &mut self,
1250 tx_result: &TransactionResult,
1251 ) -> Result<ProvenTransaction, ClientError> {
1252 self.prove_transaction(tx_result, self.tx_prover.clone()).await
1253 }
1254
1255 pub async fn testing_submit_proven_transaction(
1256 &mut self,
1257 proven_transaction: ProvenTransaction,
1258 ) -> Result<BlockNumber, ClientError> {
1259 self.submit_proven_transaction(proven_transaction).await
1260 }
1261
1262 pub async fn testing_apply_transaction(
1263 &self,
1264 tx_result: TransactionResult,
1265 ) -> Result<(), ClientError> {
1266 self.apply_transaction(self.get_sync_height().await.unwrap(), tx_result).await
1267 }
1268}
1269
1270fn collect_assets<'a>(
1274 assets: impl Iterator<Item = &'a Asset>,
1275) -> (BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>) {
1276 let mut fungible_balance_map = BTreeMap::new();
1277 let mut non_fungible_set = BTreeSet::new();
1278
1279 assets.for_each(|asset| match asset {
1280 Asset::Fungible(fungible) => {
1281 fungible_balance_map
1282 .entry(fungible.faucet_id())
1283 .and_modify(|balance| *balance += fungible.amount())
1284 .or_insert(fungible.amount());
1285 },
1286 Asset::NonFungible(non_fungible) => {
1287 non_fungible_set.insert(*non_fungible);
1288 },
1289 });
1290
1291 (fungible_balance_map, non_fungible_set)
1292}
1293
1294pub fn notes_from_output(output_notes: &OutputNotes) -> impl Iterator<Item = &Note> {
1299 output_notes
1300 .iter()
1301 .filter(|n| matches!(n, OutputNote::Full(_)))
1302 .map(|n| match n {
1303 OutputNote::Full(n) => n,
1304 OutputNote::Header(_) | OutputNote::Partial(_) => {
1307 todo!("For now, all details should be held in OutputNote::Fulls")
1308 },
1309 })
1310}
1311
1312fn validate_executed_transaction(
1315 executed_transaction: &ExecutedTransaction,
1316 expected_output_recipients: &[NoteRecipient],
1317) -> Result<(), ClientError> {
1318 let tx_output_recipient_digests = executed_transaction
1319 .output_notes()
1320 .iter()
1321 .filter_map(|n| n.recipient().map(NoteRecipient::digest))
1322 .collect::<Vec<_>>();
1323
1324 let missing_recipient_digest: Vec<Word> = expected_output_recipients
1325 .iter()
1326 .filter_map(|recipient| {
1327 (!tx_output_recipient_digests.contains(&recipient.digest()))
1328 .then_some(recipient.digest())
1329 })
1330 .collect();
1331
1332 if !missing_recipient_digest.is_empty() {
1333 return Err(ClientError::MissingOutputRecipients(missing_recipient_digest));
1334 }
1335
1336 Ok(())
1337}
1338
1339#[cfg(test)]
1340mod test {
1341 use alloc::boxed::Box;
1342
1343 use miden_lib::account::auth::AuthRpoFalcon512;
1344 use miden_lib::transaction::TransactionKernel;
1345 use miden_objects::Word;
1346 use miden_objects::account::{
1347 AccountBuilder,
1348 AccountComponent,
1349 AuthSecretKey,
1350 StorageMap,
1351 StorageSlot,
1352 };
1353 use miden_objects::asset::{Asset, FungibleAsset};
1354 use miden_objects::crypto::dsa::rpo_falcon512::SecretKey;
1355 use miden_objects::note::NoteType;
1356 use miden_objects::testing::account_id::{
1357 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
1358 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
1359 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
1360 };
1361 use miden_tx::utils::{Deserializable, Serializable};
1362
1363 use super::PaymentNoteDescription;
1364 use crate::tests::create_test_client;
1365 use crate::transaction::{TransactionRequestBuilder, TransactionResult};
1366
1367 #[tokio::test]
1368 async fn transaction_creates_two_notes() {
1369 let (mut client, _, keystore) = Box::pin(create_test_client()).await;
1370 let asset_1: Asset =
1371 FungibleAsset::new(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into().unwrap(), 123)
1372 .unwrap()
1373 .into();
1374 let asset_2: Asset =
1375 FungibleAsset::new(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap(), 500)
1376 .unwrap()
1377 .into();
1378
1379 let secret_key = SecretKey::new();
1380 let pub_key = secret_key.public_key();
1381 keystore.add_key(&AuthSecretKey::RpoFalcon512(secret_key)).unwrap();
1382
1383 let wallet_component = AccountComponent::compile(
1384 "
1385 export.::miden::contracts::wallets::basic::receive_asset
1386 export.::miden::contracts::wallets::basic::move_asset_to_note
1387 ",
1388 TransactionKernel::assembler(),
1389 vec![StorageSlot::Value(Word::default()), StorageSlot::Map(StorageMap::default())],
1390 )
1391 .unwrap()
1392 .with_supports_all_types();
1393
1394 let account = AccountBuilder::new(Default::default())
1395 .with_component(wallet_component)
1396 .with_auth_component(AuthRpoFalcon512::new(pub_key))
1397 .with_assets([asset_1, asset_2])
1398 .build_existing()
1399 .unwrap();
1400
1401 client.add_account(&account, None, false).await.unwrap();
1402 client.sync_state().await.unwrap();
1403 let tx_request = TransactionRequestBuilder::new()
1404 .build_pay_to_id(
1405 PaymentNoteDescription::new(
1406 vec![asset_1, asset_2],
1407 account.id(),
1408 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into().unwrap(),
1409 ),
1410 NoteType::Private,
1411 client.rng(),
1412 )
1413 .unwrap();
1414
1415 let tx_result = Box::pin(client.new_transaction(account.id(), tx_request)).await.unwrap();
1416 assert!(
1417 tx_result
1418 .created_notes()
1419 .get_note(0)
1420 .assets()
1421 .is_some_and(|assets| assets.num_assets() == 2)
1422 );
1423 Box::pin(client.testing_apply_transaction(tx_result.clone())).await.unwrap();
1425
1426 let bytes: std::vec::Vec<u8> = tx_result.to_bytes();
1428 let decoded = TransactionResult::read_from_bytes(&bytes).unwrap();
1429
1430 assert_eq!(tx_result, decoded);
1431 }
1432}