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