1use alloc::{
66 collections::{BTreeMap, BTreeSet},
67 string::ToString,
68 sync::Arc,
69 vec::Vec,
70};
71use core::fmt::{self};
72
73use miden_objects::{
74 AssetError, Digest, Felt, Word,
75 account::{Account, AccountCode, AccountDelta, AccountId},
76 assembly::DefaultSourceManager,
77 asset::{Asset, NonFungibleAsset},
78 block::BlockNumber,
79 note::{Note, NoteDetails, NoteId, NoteTag},
80 transaction::{AccountInputs, TransactionArgs},
81};
82use miden_tx::{
83 NoteAccountExecution, NoteConsumptionChecker,
84 utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
85};
86use tracing::info;
87
88use super::Client;
89use crate::{
90 ClientError,
91 note::{NoteScreener, NoteUpdateTracker},
92 rpc::domain::account::AccountProof,
93 store::{
94 InputNoteRecord, InputNoteState, NoteFilter, OutputNoteRecord, StoreError,
95 TransactionFilter, input_note_states::ExpectedNoteState,
96 },
97 sync::NoteTagRecord,
98};
99
100mod request;
101
102pub use miden_lib::{
106 account::interface::{AccountComponentInterface, AccountInterface},
107 transaction::TransactionKernel,
108};
109pub use miden_objects::{
110 transaction::{
111 ExecutedTransaction, InputNote, InputNotes, OutputNote, OutputNotes, ProvenTransaction,
112 TransactionId, TransactionScript,
113 },
114 vm::{AdviceInputs, AdviceMap},
115};
116pub use miden_tx::{
117 DataStoreError, LocalTransactionProver, ProvingOptions, TransactionExecutorError,
118 TransactionProver, TransactionProverError, auth::TransactionAuthenticator,
119};
120pub use request::{
121 ForeignAccount, NoteArgs, PaymentTransactionData, SwapTransactionData, TransactionRequest,
122 TransactionRequestBuilder, TransactionRequestError, TransactionScriptTemplate,
123};
124
125#[derive(Clone, Debug, PartialEq)]
135pub struct TransactionResult {
136 transaction: ExecutedTransaction,
137 relevant_notes: Vec<InputNoteRecord>,
138}
139
140impl TransactionResult {
141 pub async fn new(
144 transaction: ExecutedTransaction,
145 note_screener: NoteScreener<'_>,
146 partial_notes: Vec<(NoteDetails, NoteTag)>,
147 current_block_num: BlockNumber,
148 current_timestamp: Option<u64>,
149 ) -> Result<Self, ClientError> {
150 let mut relevant_notes = vec![];
151
152 for note in notes_from_output(transaction.output_notes()) {
153 let account_relevance = note_screener.check_relevance(note).await?;
154 if !account_relevance.is_empty() {
155 let metadata = *note.metadata();
156 relevant_notes.push(InputNoteRecord::new(
157 note.into(),
158 current_timestamp,
159 ExpectedNoteState {
160 metadata: Some(metadata),
161 after_block_num: current_block_num,
162 tag: Some(metadata.tag()),
163 }
164 .into(),
165 ));
166 }
167 }
168
169 relevant_notes.extend(partial_notes.iter().map(|(note_details, tag)| {
171 InputNoteRecord::new(
172 note_details.clone(),
173 None,
174 ExpectedNoteState {
175 metadata: None,
176 after_block_num: current_block_num,
177 tag: Some(*tag),
178 }
179 .into(),
180 )
181 }));
182
183 let tx_result = Self { transaction, relevant_notes };
184
185 Ok(tx_result)
186 }
187
188 pub fn executed_transaction(&self) -> &ExecutedTransaction {
190 &self.transaction
191 }
192
193 pub fn created_notes(&self) -> &OutputNotes {
195 self.transaction.output_notes()
196 }
197
198 pub fn relevant_notes(&self) -> &[InputNoteRecord] {
200 &self.relevant_notes
201 }
202
203 pub fn block_num(&self) -> BlockNumber {
205 self.transaction.block_header().block_num()
206 }
207
208 pub fn transaction_arguments(&self) -> &TransactionArgs {
210 self.transaction.tx_args()
211 }
212
213 pub fn account_delta(&self) -> &AccountDelta {
215 self.transaction.account_delta()
216 }
217
218 pub fn consumed_notes(&self) -> &InputNotes<InputNote> {
220 self.transaction.tx_inputs().input_notes()
221 }
222}
223
224impl From<TransactionResult> for ExecutedTransaction {
225 fn from(tx_result: TransactionResult) -> ExecutedTransaction {
226 tx_result.transaction
227 }
228}
229
230impl Serializable for TransactionResult {
231 fn write_into<W: ByteWriter>(&self, target: &mut W) {
232 self.transaction.write_into(target);
233 self.relevant_notes.write_into(target);
234 }
235}
236
237impl Deserializable for TransactionResult {
238 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
239 let transaction = ExecutedTransaction::read_from(source)?;
240 let relevant_notes = Vec::<InputNoteRecord>::read_from(source)?;
241
242 Ok(Self { transaction, relevant_notes })
243 }
244}
245
246#[derive(Debug, Clone)]
251pub struct TransactionRecord {
252 pub id: TransactionId,
254 pub details: TransactionDetails,
256 pub script: Option<TransactionScript>,
259 pub status: TransactionStatus,
261}
262
263impl TransactionRecord {
264 pub fn new(
266 id: TransactionId,
267 details: TransactionDetails,
268 script: Option<TransactionScript>,
269 status: TransactionStatus,
270 ) -> TransactionRecord {
271 TransactionRecord { id, details, script, status }
272 }
273
274 pub fn commit_transaction(&mut self, commit_height: BlockNumber) -> bool {
277 match self.status {
278 TransactionStatus::Pending => {
279 self.status = TransactionStatus::Committed(commit_height);
280 true
281 },
282 TransactionStatus::Discarded(_) | TransactionStatus::Committed(_) => false,
283 }
284 }
285
286 pub fn discard_transaction(&mut self, cause: DiscardCause) -> bool {
289 match self.status {
290 TransactionStatus::Pending => {
291 self.status = TransactionStatus::Discarded(cause);
292 true
293 },
294 TransactionStatus::Discarded(_) | TransactionStatus::Committed(_) => false,
295 }
296 }
297}
298
299#[derive(Debug, Clone)]
301pub struct TransactionDetails {
302 pub account_id: AccountId,
304 pub init_account_state: Digest,
306 pub final_account_state: Digest,
308 pub input_note_nullifiers: Vec<Digest>,
310 pub output_notes: OutputNotes,
312 pub block_num: BlockNumber,
314 pub submission_height: BlockNumber,
316 pub expiration_block_num: BlockNumber,
318}
319
320impl Serializable for TransactionDetails {
321 fn write_into<W: ByteWriter>(&self, target: &mut W) {
322 self.account_id.write_into(target);
323 self.init_account_state.write_into(target);
324 self.final_account_state.write_into(target);
325 self.input_note_nullifiers.write_into(target);
326 self.output_notes.write_into(target);
327 self.block_num.write_into(target);
328 self.submission_height.write_into(target);
329 self.expiration_block_num.write_into(target);
330 }
331}
332
333impl Deserializable for TransactionDetails {
334 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
335 let account_id = AccountId::read_from(source)?;
336 let init_account_state = Digest::read_from(source)?;
337 let final_account_state = Digest::read_from(source)?;
338 let input_note_nullifiers = Vec::<Digest>::read_from(source)?;
339 let output_notes = OutputNotes::read_from(source)?;
340 let block_num = BlockNumber::read_from(source)?;
341 let submission_height = BlockNumber::read_from(source)?;
342 let expiration_block_num = BlockNumber::read_from(source)?;
343
344 Ok(Self {
345 account_id,
346 init_account_state,
347 final_account_state,
348 input_note_nullifiers,
349 output_notes,
350 block_num,
351 submission_height,
352 expiration_block_num,
353 })
354 }
355}
356
357#[derive(Debug, Clone, Copy, PartialEq)]
359pub enum DiscardCause {
360 Expired,
361 InputConsumed,
362 DiscardedInitialState,
363 Stale,
364}
365
366impl DiscardCause {
367 pub fn from_string(cause: &str) -> Result<Self, DeserializationError> {
368 match cause {
369 "Expired" => Ok(DiscardCause::Expired),
370 "InputConsumed" => Ok(DiscardCause::InputConsumed),
371 "DiscardedInitialState" => Ok(DiscardCause::DiscardedInitialState),
372 "Stale" => Ok(DiscardCause::Stale),
373 _ => Err(DeserializationError::InvalidValue(format!("Invalid discard cause: {cause}"))),
374 }
375 }
376}
377
378impl fmt::Display for DiscardCause {
379 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
380 match self {
381 DiscardCause::Expired => write!(f, "Expired"),
382 DiscardCause::InputConsumed => write!(f, "InputConsumed"),
383 DiscardCause::DiscardedInitialState => write!(f, "DiscardedInitialState"),
384 DiscardCause::Stale => write!(f, "Stale"),
385 }
386 }
387}
388
389impl Serializable for DiscardCause {
390 fn write_into<W: ByteWriter>(&self, target: &mut W) {
391 match self {
392 DiscardCause::Expired => target.write_u8(0),
393 DiscardCause::InputConsumed => target.write_u8(1),
394 DiscardCause::DiscardedInitialState => target.write_u8(2),
395 DiscardCause::Stale => target.write_u8(3),
396 }
397 }
398}
399
400impl Deserializable for DiscardCause {
401 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
402 match source.read_u8()? {
403 0 => Ok(DiscardCause::Expired),
404 1 => Ok(DiscardCause::InputConsumed),
405 2 => Ok(DiscardCause::DiscardedInitialState),
406 3 => Ok(DiscardCause::Stale),
407 _ => Err(DeserializationError::InvalidValue("Invalid discard cause".to_string())),
408 }
409 }
410}
411
412#[derive(Debug, Clone, PartialEq)]
414pub enum TransactionStatus {
415 Pending,
417 Committed(BlockNumber),
419 Discarded(DiscardCause),
421}
422
423impl fmt::Display for TransactionStatus {
424 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
425 match self {
426 TransactionStatus::Pending => write!(f, "Pending"),
427 TransactionStatus::Committed(block_number) => {
428 write!(f, "Committed (Block: {block_number})")
429 },
430 TransactionStatus::Discarded(cause) => write!(f, "Discarded ({cause})",),
431 }
432 }
433}
434
435pub struct TransactionStoreUpdate {
441 executed_transaction: ExecutedTransaction,
443 submission_height: BlockNumber,
445 updated_account: Account,
447 note_updates: NoteUpdateTracker,
449 new_tags: Vec<NoteTagRecord>,
451}
452
453impl TransactionStoreUpdate {
454 pub fn new(
464 executed_transaction: ExecutedTransaction,
465 submission_height: BlockNumber,
466 updated_account: Account,
467 note_updates: NoteUpdateTracker,
468 new_tags: Vec<NoteTagRecord>,
469 ) -> Self {
470 Self {
471 executed_transaction,
472 submission_height,
473 updated_account,
474 note_updates,
475 new_tags,
476 }
477 }
478
479 pub fn executed_transaction(&self) -> &ExecutedTransaction {
481 &self.executed_transaction
482 }
483
484 pub fn submission_height(&self) -> BlockNumber {
486 self.submission_height
487 }
488
489 pub fn updated_account(&self) -> &Account {
491 &self.updated_account
492 }
493
494 pub fn note_updates(&self) -> &NoteUpdateTracker {
496 &self.note_updates
497 }
498
499 pub fn new_tags(&self) -> &[NoteTagRecord] {
501 &self.new_tags
502 }
503}
504
505impl Client {
507 pub async fn get_transactions(
512 &self,
513 filter: TransactionFilter,
514 ) -> Result<Vec<TransactionRecord>, ClientError> {
515 self.store.get_transactions(filter).await.map_err(Into::into)
516 }
517
518 pub async fn new_transaction(
535 &mut self,
536 account_id: AccountId,
537 transaction_request: TransactionRequest,
538 ) -> Result<TransactionResult, ClientError> {
539 self.validate_request(account_id, &transaction_request).await?;
541
542 let authenticated_input_note_ids: Vec<NoteId> =
545 transaction_request.authenticated_input_note_ids().collect::<Vec<_>>();
546
547 let authenticated_note_records = self
548 .store
549 .get_input_notes(NoteFilter::List(authenticated_input_note_ids))
550 .await?;
551
552 for authenticated_note_record in authenticated_note_records {
553 if !authenticated_note_record.is_authenticated() {
554 return Err(ClientError::TransactionRequestError(
555 TransactionRequestError::InputNoteNotAuthenticated(
556 authenticated_note_record.id(),
557 ),
558 ));
559 }
560
561 if authenticated_note_record.is_consumed() {
562 return Err(ClientError::TransactionRequestError(
563 TransactionRequestError::InputNoteAlreadyConsumed(
564 authenticated_note_record.id(),
565 ),
566 ));
567 }
568 }
569
570 let unauthenticated_input_notes = transaction_request
572 .unauthenticated_input_notes()
573 .iter()
574 .cloned()
575 .map(Into::into)
576 .collect::<Vec<_>>();
577
578 self.store.upsert_input_notes(&unauthenticated_input_notes).await?;
579
580 let mut notes = {
581 let note_ids = transaction_request.get_input_note_ids();
582
583 let mut input_notes: Vec<InputNote> = Vec::new();
584
585 for note in self.store.get_input_notes(NoteFilter::List(note_ids)).await? {
586 input_notes.push(note.try_into().map_err(ClientError::NoteRecordConversionError)?);
587 }
588
589 InputNotes::new(input_notes).map_err(ClientError::TransactionInputError)?
590 };
591
592 let output_notes: Vec<Note> =
593 transaction_request.expected_output_notes().cloned().collect();
594
595 let future_notes: Vec<(NoteDetails, NoteTag)> =
596 transaction_request.expected_future_notes().cloned().collect();
597
598 let tx_script = transaction_request.build_transaction_script(
599 &self.get_account_interface(account_id).await?,
600 self.in_debug_mode,
601 )?;
602
603 let foreign_accounts = transaction_request.foreign_accounts().clone();
604
605 let (fpi_block_num, foreign_account_inputs) =
607 self.retrieve_foreign_account_inputs(foreign_accounts).await?;
608
609 let ignore_invalid_notes = transaction_request.ignore_invalid_input_notes();
610
611 let tx_args = transaction_request.into_transaction_args(tx_script, foreign_account_inputs);
612
613 let block_num = if let Some(block_num) = fpi_block_num {
614 block_num
615 } else {
616 self.store.get_sync_height().await?
617 };
618
619 let account_record = self
621 .store
622 .get_account(account_id)
623 .await?
624 .ok_or(ClientError::AccountDataNotFound(account_id))?;
625 let account: Account = account_record.into();
626 self.mast_store.load_transaction_code(account.code(), ¬es, &tx_args);
627
628 if ignore_invalid_notes {
629 notes = self.get_valid_input_notes(account_id, notes, tx_args.clone()).await?;
631 }
632
633 let executed_transaction = self
635 .tx_executor
636 .execute_transaction(
637 account_id,
638 block_num,
639 notes,
640 tx_args,
641 Arc::new(DefaultSourceManager::default()), )
643 .await?;
644
645 let tx_note_auth_commitments: BTreeSet<Digest> =
651 notes_from_output(executed_transaction.output_notes())
652 .map(Note::commitment)
653 .collect();
654
655 let missing_note_ids: Vec<NoteId> = output_notes
656 .iter()
657 .filter_map(|n| (!tx_note_auth_commitments.contains(&n.commitment())).then_some(n.id()))
658 .collect();
659
660 if !missing_note_ids.is_empty() {
661 return Err(ClientError::MissingOutputNotes(missing_note_ids));
662 }
663
664 let screener =
665 NoteScreener::new(self.store.clone(), &self.tx_executor, self.mast_store.clone());
666
667 TransactionResult::new(
668 executed_transaction,
669 screener,
670 future_notes,
671 self.get_sync_height().await?,
672 self.store.get_current_timestamp(),
673 )
674 .await
675 }
676
677 pub async fn submit_transaction(
680 &mut self,
681 tx_result: TransactionResult,
682 ) -> Result<(), ClientError> {
683 self.submit_transaction_with_prover(tx_result, self.tx_prover.clone()).await
684 }
685
686 pub async fn submit_transaction_with_prover(
689 &mut self,
690 tx_result: TransactionResult,
691 tx_prover: Arc<dyn TransactionProver>,
692 ) -> Result<(), ClientError> {
693 let proven_transaction = self.prove_transaction(&tx_result, tx_prover).await?;
694 let block_num = self.submit_proven_transaction(proven_transaction).await?;
695 self.apply_transaction(block_num, tx_result).await
696 }
697
698 async fn prove_transaction(
700 &mut self,
701 tx_result: &TransactionResult,
702 tx_prover: Arc<dyn TransactionProver>,
703 ) -> Result<ProvenTransaction, ClientError> {
704 info!("Proving transaction...");
705
706 let proven_transaction =
707 tx_prover.prove(tx_result.executed_transaction().clone().into()).await?;
708
709 info!("Transaction proven.");
710
711 Ok(proven_transaction)
712 }
713
714 async fn submit_proven_transaction(
715 &mut self,
716 proven_transaction: ProvenTransaction,
717 ) -> Result<BlockNumber, ClientError> {
718 info!("Submitting transaction to the network...");
719 let block_num = self.rpc_api.submit_proven_transaction(proven_transaction).await?;
720 info!("Transaction submitted.");
721
722 Ok(block_num)
723 }
724
725 async fn apply_transaction(
726 &self,
727 submission_height: BlockNumber,
728 tx_result: TransactionResult,
729 ) -> Result<(), ClientError> {
730 let transaction_id = tx_result.executed_transaction().id();
731
732 info!("Applying transaction to the local store...");
735
736 let account_id = tx_result.executed_transaction().account_id();
737 let account_delta = tx_result.account_delta();
738 let account_record = self.try_get_account(account_id).await?;
739
740 if account_record.is_locked() {
741 return Err(ClientError::AccountLocked(account_id));
742 }
743
744 let mut account: Account = account_record.into();
745 account.apply_delta(account_delta)?;
746
747 if self
748 .store
749 .get_account_header_by_commitment(account.commitment())
750 .await?
751 .is_some()
752 {
753 return Err(ClientError::StoreError(StoreError::AccountCommitmentAlreadyExists(
754 account.commitment(),
755 )));
756 }
757
758 let created_input_notes = tx_result.relevant_notes().to_vec();
760 let new_tags = created_input_notes
761 .iter()
762 .filter_map(|note| {
763 if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) =
764 note.state()
765 {
766 Some(NoteTagRecord::with_note_source(*tag, note.id()))
767 } else {
768 None
769 }
770 })
771 .collect();
772
773 let created_output_notes = tx_result
775 .created_notes()
776 .iter()
777 .cloned()
778 .filter_map(|output_note| {
779 OutputNoteRecord::try_from_output_note(output_note, submission_height).ok()
780 })
781 .collect::<Vec<_>>();
782
783 let consumed_note_ids = tx_result.consumed_notes().iter().map(InputNote::id).collect();
784 let consumed_notes = self.get_input_notes(NoteFilter::List(consumed_note_ids)).await?;
785
786 let mut updated_input_notes = vec![];
787 for mut input_note_record in consumed_notes {
788 if input_note_record.consumed_locally(
789 account_id,
790 transaction_id,
791 self.store.get_current_timestamp(),
792 )? {
793 updated_input_notes.push(input_note_record);
794 }
795 }
796
797 let note_updates = NoteUpdateTracker::for_transaction_updates(
798 created_input_notes,
799 updated_input_notes,
800 created_output_notes,
801 );
802
803 let tx_update = TransactionStoreUpdate::new(
804 tx_result.into(),
805 submission_height,
806 account,
807 note_updates,
808 new_tags,
809 );
810
811 self.store.apply_transaction(tx_update).await?;
812 info!("Transaction stored.");
813 Ok(())
814 }
815
816 pub fn compile_tx_script<T>(
818 &self,
819 inputs: T,
820 program: &str,
821 ) -> Result<TransactionScript, ClientError>
822 where
823 T: IntoIterator<Item = (Word, Vec<Felt>)>,
824 {
825 let assembler = TransactionKernel::assembler().with_debug_mode(self.in_debug_mode);
826 TransactionScript::compile(program, inputs, assembler)
827 .map_err(ClientError::TransactionScriptError)
828 }
829
830 fn get_outgoing_assets(
838 transaction_request: &TransactionRequest,
839 ) -> (BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>) {
840 let mut own_notes_assets = match transaction_request.script_template() {
842 Some(TransactionScriptTemplate::SendNotes(notes)) => {
843 notes.iter().map(|note| (note.id(), note.assets())).collect::<BTreeMap<_, _>>()
844 },
845 _ => BTreeMap::default(),
846 };
847 let mut output_notes_assets = transaction_request
849 .expected_output_notes()
850 .map(|note| (note.id(), note.assets()))
851 .collect::<BTreeMap<_, _>>();
852
853 output_notes_assets.append(&mut own_notes_assets);
855
856 let outgoing_assets =
858 output_notes_assets.values().flat_map(|note_assets| note_assets.iter());
859
860 collect_assets(outgoing_assets)
861 }
862
863 async fn get_incoming_assets(
865 &self,
866 transaction_request: &TransactionRequest,
867 ) -> Result<(BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>), TransactionRequestError>
868 {
869 let incoming_notes_ids: Vec<_> = transaction_request
871 .input_notes()
872 .iter()
873 .filter_map(|(note_id, _)| {
874 if transaction_request
875 .unauthenticated_input_notes()
876 .iter()
877 .any(|note| note.id() == *note_id)
878 {
879 None
880 } else {
881 Some(*note_id)
882 }
883 })
884 .collect();
885
886 let store_input_notes = self
887 .get_input_notes(NoteFilter::List(incoming_notes_ids))
888 .await
889 .map_err(|err| TransactionRequestError::NoteNotFound(err.to_string()))?;
890
891 let all_incoming_assets =
892 store_input_notes.iter().flat_map(|note| note.assets().iter()).chain(
893 transaction_request
894 .unauthenticated_input_notes()
895 .iter()
896 .flat_map(|note| note.assets().iter()),
897 );
898
899 Ok(collect_assets(all_incoming_assets))
900 }
901
902 async fn validate_basic_account_request(
903 &self,
904 transaction_request: &TransactionRequest,
905 account: &Account,
906 ) -> Result<(), ClientError> {
907 let (fungible_balance_map, non_fungible_set) =
909 Client::get_outgoing_assets(transaction_request);
910
911 let (incoming_fungible_balance_map, incoming_non_fungible_balance_set) =
913 self.get_incoming_assets(transaction_request).await?;
914
915 for (faucet_id, amount) in fungible_balance_map {
918 let account_asset_amount = account.vault().get_balance(faucet_id).unwrap_or(0);
919 let incoming_balance = incoming_fungible_balance_map.get(&faucet_id).unwrap_or(&0);
920 if account_asset_amount + incoming_balance < amount {
921 return Err(ClientError::AssetError(
922 AssetError::FungibleAssetAmountNotSufficient {
923 minuend: account_asset_amount,
924 subtrahend: amount,
925 },
926 ));
927 }
928 }
929
930 for non_fungible in non_fungible_set {
933 match account.vault().has_non_fungible_asset(non_fungible) {
934 Ok(true) => (),
935 Ok(false) => {
936 if !incoming_non_fungible_balance_set.contains(&non_fungible) {
938 return Err(ClientError::AssetError(
939 AssetError::NonFungibleFaucetIdTypeMismatch(
940 non_fungible.faucet_id_prefix(),
941 ),
942 ));
943 }
944 },
945 _ => {
946 return Err(ClientError::AssetError(
947 AssetError::NonFungibleFaucetIdTypeMismatch(
948 non_fungible.faucet_id_prefix(),
949 ),
950 ));
951 },
952 }
953 }
954
955 Ok(())
956 }
957
958 pub async fn validate_request(
965 &mut self,
966 account_id: AccountId,
967 transaction_request: &TransactionRequest,
968 ) -> Result<(), ClientError> {
969 let current_chain_tip =
970 self.rpc_api.get_block_header_by_number(None, false).await?.0.block_num();
971
972 if let Some(max_block_number_delta) = self.max_block_number_delta {
973 if current_chain_tip > self.store.get_sync_height().await? + max_block_number_delta {
974 return Err(ClientError::RecencyConditionError(
975 "The client is too far behind the chain tip to execute the transaction"
976 .to_string(),
977 ));
978 }
979 }
980
981 let account: Account = self.try_get_account(account_id).await?.into();
982
983 if account.is_faucet() {
984 Ok(())
986 } else {
987 self.validate_basic_account_request(transaction_request, &account).await
988 }
989 }
990
991 async fn get_valid_input_notes(
992 &self,
993 account_id: AccountId,
994 mut input_notes: InputNotes<InputNote>,
995 tx_args: TransactionArgs,
996 ) -> Result<InputNotes<InputNote>, ClientError> {
997 loop {
998 let execution = NoteConsumptionChecker::new(&self.tx_executor)
999 .check_notes_consumability(
1000 account_id,
1001 self.store.get_sync_height().await?,
1002 input_notes.clone(),
1003 tx_args.clone(),
1004 Arc::new(DefaultSourceManager::default()),
1005 )
1006 .await?;
1007
1008 if let NoteAccountExecution::Failure { failed_note_id, .. } = execution {
1009 let filtered_input_notes = InputNotes::new(
1010 input_notes.into_iter().filter(|note| note.id() != failed_note_id).collect(),
1011 )
1012 .expect("Created from a valid input notes list");
1013
1014 input_notes = filtered_input_notes;
1015 } else {
1016 break;
1017 }
1018 }
1019
1020 Ok(input_notes)
1021 }
1022
1023 pub(crate) async fn get_account_interface(
1025 &self,
1026 account_id: AccountId,
1027 ) -> Result<AccountInterface, ClientError> {
1028 let account: Account = self.try_get_account(account_id).await?.into();
1029
1030 Ok(AccountInterface::from(&account))
1031 }
1032
1033 async fn retrieve_foreign_account_inputs(
1044 &mut self,
1045 foreign_accounts: BTreeSet<ForeignAccount>,
1046 ) -> Result<(Option<BlockNumber>, Vec<AccountInputs>), ClientError> {
1047 if foreign_accounts.is_empty() {
1048 return Ok((None, Vec::new()));
1049 }
1050
1051 let mut return_foreign_account_inputs = Vec::with_capacity(foreign_accounts.len());
1052
1053 let account_ids = foreign_accounts.iter().map(ForeignAccount::account_id);
1054 let known_account_codes =
1055 self.store.get_foreign_account_code(account_ids.collect()).await?;
1056
1057 let known_account_codes: Vec<AccountCode> = known_account_codes.into_values().collect();
1058
1059 let (block_num, account_proofs) =
1061 self.rpc_api.get_account_proofs(&foreign_accounts, known_account_codes).await?;
1062
1063 let mut account_proofs: BTreeMap<AccountId, AccountProof> =
1064 account_proofs.into_iter().map(|proof| (proof.account_id(), proof)).collect();
1065
1066 for foreign_account in &foreign_accounts {
1067 let foreign_account_inputs = match foreign_account {
1068 ForeignAccount::Public(account_id, ..) => {
1069 let account_proof = account_proofs
1070 .remove(account_id)
1071 .expect("proof was requested and received");
1072
1073 let foreign_account_inputs: AccountInputs = account_proof.try_into()?;
1074
1075 self.store
1077 .upsert_foreign_account_code(
1078 *account_id,
1079 foreign_account_inputs.code().clone(),
1080 )
1081 .await?;
1082
1083 foreign_account_inputs
1084 },
1085 ForeignAccount::Private(partial_account) => {
1086 let account_id = partial_account.id();
1087 let (witness, _) = account_proofs
1088 .remove(&account_id)
1089 .expect("proof was requested and received")
1090 .into_parts();
1091
1092 AccountInputs::new(partial_account.clone(), witness)
1093 },
1094 };
1095
1096 return_foreign_account_inputs.push(foreign_account_inputs);
1097 }
1098
1099 if self.store.get_block_header_by_num(block_num).await?.is_none() {
1101 info!(
1102 "Getting current block header data to execute transaction with foreign account requirements"
1103 );
1104 let summary = self.sync_state().await?;
1105
1106 if summary.block_num != block_num {
1107 let mut current_partial_mmr = self.build_current_partial_mmr().await?;
1108 self.get_and_store_authenticated_block(block_num, &mut current_partial_mmr)
1109 .await?;
1110 }
1111 }
1112
1113 Ok((Some(block_num), return_foreign_account_inputs))
1114 }
1115
1116 pub async fn execute_program(
1121 &mut self,
1122 account_id: AccountId,
1123 tx_script: TransactionScript,
1124 advice_inputs: AdviceInputs,
1125 foreign_accounts: BTreeSet<ForeignAccount>,
1126 ) -> Result<[Felt; 16], ClientError> {
1127 let (fpi_block_number, foreign_account_inputs) =
1128 self.retrieve_foreign_account_inputs(foreign_accounts).await?;
1129 let block_ref = if let Some(block_number) = fpi_block_number {
1130 block_number
1131 } else {
1132 self.get_sync_height().await?
1133 };
1134
1135 let account_record = self
1136 .store
1137 .get_account(account_id)
1138 .await?
1139 .ok_or(ClientError::AccountDataNotFound(account_id))?;
1140 let account: Account = account_record.into();
1141
1142 self.mast_store.insert(tx_script.mast());
1144 self.mast_store.insert(account.code().mast());
1145 for fpi_account in &foreign_account_inputs {
1146 self.mast_store.insert(fpi_account.code().mast());
1147 }
1148
1149 Ok(self
1150 .tx_executor
1151 .execute_tx_view_script(
1152 account_id,
1153 block_ref,
1154 tx_script,
1155 advice_inputs,
1156 foreign_account_inputs,
1157 Arc::new(DefaultSourceManager::default()), )
1159 .await?)
1160 }
1161}
1162
1163#[cfg(feature = "testing")]
1167impl Client {
1168 pub async fn testing_prove_transaction(
1169 &mut self,
1170 tx_result: &TransactionResult,
1171 ) -> Result<ProvenTransaction, ClientError> {
1172 self.prove_transaction(tx_result, self.tx_prover.clone()).await
1173 }
1174
1175 pub async fn testing_submit_proven_transaction(
1176 &mut self,
1177 proven_transaction: ProvenTransaction,
1178 ) -> Result<BlockNumber, ClientError> {
1179 self.submit_proven_transaction(proven_transaction).await
1180 }
1181
1182 pub async fn testing_apply_transaction(
1183 &self,
1184 tx_result: TransactionResult,
1185 ) -> Result<(), ClientError> {
1186 self.apply_transaction(self.get_sync_height().await.unwrap(), tx_result).await
1187 }
1188}
1189
1190fn collect_assets<'a>(
1194 assets: impl Iterator<Item = &'a Asset>,
1195) -> (BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>) {
1196 let mut fungible_balance_map = BTreeMap::new();
1197 let mut non_fungible_set = BTreeSet::new();
1198
1199 assets.for_each(|asset| match asset {
1200 Asset::Fungible(fungible) => {
1201 fungible_balance_map
1202 .entry(fungible.faucet_id())
1203 .and_modify(|balance| *balance += fungible.amount())
1204 .or_insert(fungible.amount());
1205 },
1206 Asset::NonFungible(non_fungible) => {
1207 non_fungible_set.insert(*non_fungible);
1208 },
1209 });
1210
1211 (fungible_balance_map, non_fungible_set)
1212}
1213
1214pub fn notes_from_output(output_notes: &OutputNotes) -> impl Iterator<Item = &Note> {
1219 output_notes
1220 .iter()
1221 .filter(|n| matches!(n, OutputNote::Full(_)))
1222 .map(|n| match n {
1223 OutputNote::Full(n) => n,
1224 OutputNote::Header(_) | OutputNote::Partial(_) => {
1227 todo!("For now, all details should be held in OutputNote::Fulls")
1228 },
1229 })
1230}
1231
1232#[cfg(test)]
1233mod test {
1234 use miden_lib::{account::auth::RpoFalcon512, transaction::TransactionKernel};
1235 use miden_objects::{
1236 Word,
1237 account::{AccountBuilder, AccountComponent, AuthSecretKey, StorageMap, StorageSlot},
1238 asset::{Asset, FungibleAsset},
1239 crypto::dsa::rpo_falcon512::SecretKey,
1240 note::NoteType,
1241 testing::{
1242 account_component::BASIC_WALLET_CODE,
1243 account_id::{
1244 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
1245 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
1246 },
1247 },
1248 };
1249 use miden_tx::utils::{Deserializable, Serializable};
1250
1251 use super::PaymentTransactionData;
1252 use crate::{
1253 tests::create_test_client,
1254 transaction::{TransactionRequestBuilder, TransactionResult},
1255 };
1256
1257 #[tokio::test]
1258 async fn test_transaction_creates_two_notes() {
1259 let (mut client, _, keystore) = create_test_client().await;
1260 let asset_1: Asset =
1261 FungibleAsset::new(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into().unwrap(), 123)
1262 .unwrap()
1263 .into();
1264 let asset_2: Asset =
1265 FungibleAsset::new(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap(), 500)
1266 .unwrap()
1267 .into();
1268
1269 let secret_key = SecretKey::new();
1270 let pub_key = secret_key.public_key();
1271 keystore.add_key(&AuthSecretKey::RpoFalcon512(secret_key)).unwrap();
1272
1273 let wallet_component = AccountComponent::compile(
1274 BASIC_WALLET_CODE,
1275 TransactionKernel::assembler(),
1276 vec![StorageSlot::Value(Word::default()), StorageSlot::Map(StorageMap::default())],
1277 )
1278 .unwrap()
1279 .with_supports_all_types();
1280
1281 let anchor_block = client.get_latest_epoch_block().await.unwrap();
1282
1283 let account = AccountBuilder::new(Default::default())
1284 .anchor((&anchor_block).try_into().unwrap())
1285 .with_component(wallet_component)
1286 .with_component(RpoFalcon512::new(pub_key))
1287 .with_assets([asset_1, asset_2])
1288 .build_existing()
1289 .unwrap();
1290
1291 client.add_account(&account, None, false).await.unwrap();
1292 client.sync_state().await.unwrap();
1293 let tx_request = TransactionRequestBuilder::new()
1294 .build_pay_to_id(
1295 PaymentTransactionData::new(
1296 vec![asset_1, asset_2],
1297 account.id(),
1298 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into().unwrap(),
1299 ),
1300 None,
1301 NoteType::Private,
1302 client.rng(),
1303 )
1304 .unwrap();
1305
1306 let tx_result = client.new_transaction(account.id(), tx_request).await.unwrap();
1307 assert!(
1308 tx_result
1309 .created_notes()
1310 .get_note(0)
1311 .assets()
1312 .is_some_and(|assets| assets.num_assets() == 2)
1313 );
1314 client.testing_apply_transaction(tx_result.clone()).await.unwrap();
1316
1317 let bytes: std::vec::Vec<u8> = tx_result.to_bytes();
1319 let decoded = TransactionResult::read_from_bytes(&bytes).unwrap();
1320
1321 assert_eq!(tx_result, decoded);
1322 }
1323}