1use alloc::{
67 collections::{BTreeMap, BTreeSet},
68 string::ToString,
69 sync::Arc,
70 vec::Vec,
71};
72use core::fmt::{self};
73
74pub use miden_lib::{
75 account::interface::{AccountComponentInterface, AccountInterface},
76 transaction::TransactionKernel,
77};
78use miden_objects::{
79 AssetError, Digest, Felt, Word, ZERO,
80 account::{Account, AccountCode, AccountDelta, AccountId},
81 asset::{Asset, NonFungibleAsset},
82 block::BlockNumber,
83 crypto::merkle::MerklePath,
84 note::{Note, NoteDetails, NoteId, NoteTag},
85 transaction::{InputNotes, TransactionArgs},
86 vm::AdviceInputs,
87};
88pub use miden_tx::{
89 LocalTransactionProver, ProvingOptions, TransactionProver, TransactionProverError,
90 auth::TransactionAuthenticator,
91};
92use miden_tx::{
93 TransactionExecutor,
94 utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
95};
96use tracing::info;
97
98use super::Client;
99use crate::{
100 ClientError,
101 note::{NoteScreener, NoteUpdates},
102 rpc::domain::{account::AccountProof, transaction::TransactionUpdate},
103 store::{
104 InputNoteRecord, InputNoteState, NoteFilter, OutputNoteRecord, StoreError,
105 TransactionFilter, input_note_states::ExpectedNoteState,
106 },
107 sync::{MAX_BLOCK_NUMBER_DELTA, NoteTagRecord},
108};
109
110mod request;
111pub use miden_objects::transaction::{
112 ExecutedTransaction, InputNote, OutputNote, OutputNotes, ProvenTransaction, TransactionId,
113 TransactionScript,
114};
115pub use miden_tx::{DataStoreError, TransactionExecutorError};
116pub use request::{
117 ForeignAccount, ForeignAccountInputs, NoteArgs, PaymentTransactionData, SwapTransactionData,
118 TransactionRequest, TransactionRequestBuilder, TransactionRequestError,
119 TransactionScriptTemplate,
120};
121
122#[derive(Clone, Debug, PartialEq)]
132pub struct TransactionResult {
133 transaction: ExecutedTransaction,
134 relevant_notes: Vec<InputNoteRecord>,
135}
136
137impl TransactionResult {
138 pub async fn new(
141 transaction: ExecutedTransaction,
142 note_screener: NoteScreener,
143 partial_notes: Vec<(NoteDetails, NoteTag)>,
144 current_block_num: BlockNumber,
145 current_timestamp: Option<u64>,
146 ) -> Result<Self, ClientError> {
147 let mut relevant_notes = vec![];
148
149 for note in notes_from_output(transaction.output_notes()) {
150 let account_relevance = note_screener.check_relevance(note).await?;
151 if !account_relevance.is_empty() {
152 let metadata = *note.metadata();
153 relevant_notes.push(InputNoteRecord::new(
154 note.into(),
155 current_timestamp,
156 ExpectedNoteState {
157 metadata: Some(metadata),
158 after_block_num: current_block_num,
159 tag: Some(metadata.tag()),
160 }
161 .into(),
162 ));
163 }
164 }
165
166 relevant_notes.extend(partial_notes.iter().map(|(note_details, tag)| {
168 InputNoteRecord::new(
169 note_details.clone(),
170 None,
171 ExpectedNoteState {
172 metadata: None,
173 after_block_num: current_block_num,
174 tag: Some(*tag),
175 }
176 .into(),
177 )
178 }));
179
180 let tx_result = Self { transaction, relevant_notes };
181
182 Ok(tx_result)
183 }
184
185 pub fn executed_transaction(&self) -> &ExecutedTransaction {
187 &self.transaction
188 }
189
190 pub fn created_notes(&self) -> &OutputNotes {
192 self.transaction.output_notes()
193 }
194
195 pub fn relevant_notes(&self) -> &[InputNoteRecord] {
197 &self.relevant_notes
198 }
199
200 pub fn block_num(&self) -> BlockNumber {
202 self.transaction.block_header().block_num()
203 }
204
205 pub fn transaction_arguments(&self) -> &TransactionArgs {
207 self.transaction.tx_args()
208 }
209
210 pub fn account_delta(&self) -> &AccountDelta {
212 self.transaction.account_delta()
213 }
214
215 pub fn consumed_notes(&self) -> &InputNotes<InputNote> {
217 self.transaction.tx_inputs().input_notes()
218 }
219}
220
221impl From<TransactionResult> for ExecutedTransaction {
222 fn from(tx_result: TransactionResult) -> ExecutedTransaction {
223 tx_result.transaction
224 }
225}
226
227impl Serializable for TransactionResult {
228 fn write_into<W: ByteWriter>(&self, target: &mut W) {
229 self.transaction.write_into(target);
230 self.relevant_notes.write_into(target);
231 }
232}
233
234impl Deserializable for TransactionResult {
235 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
236 let transaction = ExecutedTransaction::read_from(source)?;
237 let relevant_notes = Vec::<InputNoteRecord>::read_from(source)?;
238
239 Ok(Self { transaction, relevant_notes })
240 }
241}
242
243#[derive(Debug, Clone)]
251pub struct TransactionRecord {
252 pub id: TransactionId,
253 pub account_id: AccountId,
254 pub init_account_state: Digest,
255 pub final_account_state: Digest,
256 pub input_note_nullifiers: Vec<Digest>,
257 pub output_notes: OutputNotes,
258 pub transaction_script: Option<TransactionScript>,
259 pub block_num: BlockNumber,
260 pub transaction_status: TransactionStatus,
261}
262
263impl TransactionRecord {
264 #[allow(clippy::too_many_arguments)]
265 pub fn new(
266 id: TransactionId,
267 account_id: AccountId,
268 init_account_state: Digest,
269 final_account_state: Digest,
270 input_note_nullifiers: Vec<Digest>,
271 output_notes: OutputNotes,
272 transaction_script: Option<TransactionScript>,
273 block_num: BlockNumber,
274 transaction_status: TransactionStatus,
275 ) -> TransactionRecord {
276 TransactionRecord {
277 id,
278 account_id,
279 init_account_state,
280 final_account_state,
281 input_note_nullifiers,
282 output_notes,
283 transaction_script,
284 block_num,
285 transaction_status,
286 }
287 }
288}
289
290#[derive(Debug, Clone, PartialEq)]
292pub enum TransactionStatus {
293 Pending,
295 Committed(BlockNumber),
297 Discarded,
299}
300
301impl fmt::Display for TransactionStatus {
302 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
303 match self {
304 TransactionStatus::Pending => write!(f, "Pending"),
305 TransactionStatus::Committed(block_number) => {
306 write!(f, "Committed (Block: {block_number})")
307 },
308 TransactionStatus::Discarded => write!(f, "Discarded"),
309 }
310 }
311}
312
313pub struct TransactionStoreUpdate {
319 executed_transaction: ExecutedTransaction,
321 updated_account: Account,
323 note_updates: NoteUpdates,
325 new_tags: Vec<NoteTagRecord>,
327}
328
329impl TransactionStoreUpdate {
330 pub fn new(
332 executed_transaction: ExecutedTransaction,
333 updated_account: Account,
334 created_input_notes: Vec<InputNoteRecord>,
335 created_output_notes: Vec<OutputNoteRecord>,
336 updated_input_notes: Vec<InputNoteRecord>,
337 new_tags: Vec<NoteTagRecord>,
338 ) -> Self {
339 Self {
340 executed_transaction,
341 updated_account,
342 note_updates: NoteUpdates::new(
343 [created_input_notes, updated_input_notes].concat(),
344 created_output_notes,
345 ),
346 new_tags,
347 }
348 }
349
350 pub fn executed_transaction(&self) -> &ExecutedTransaction {
352 &self.executed_transaction
353 }
354
355 pub fn updated_account(&self) -> &Account {
357 &self.updated_account
358 }
359
360 pub fn note_updates(&self) -> &NoteUpdates {
362 &self.note_updates
363 }
364
365 pub fn new_tags(&self) -> &[NoteTagRecord] {
367 &self.new_tags
368 }
369}
370
371#[derive(Default)]
373pub struct TransactionUpdates {
374 committed_transactions: Vec<TransactionUpdate>,
377 discarded_transactions: Vec<TransactionId>,
379 stale_transactions: Vec<TransactionRecord>,
385}
386
387impl TransactionUpdates {
388 pub fn new(
390 committed_transactions: Vec<TransactionUpdate>,
391 discarded_transactions: Vec<TransactionId>,
392 stale_transactions: Vec<TransactionRecord>,
393 ) -> Self {
394 Self {
395 committed_transactions,
396 discarded_transactions,
397 stale_transactions,
398 }
399 }
400
401 pub fn extend(&mut self, other: Self) {
403 self.committed_transactions.extend(other.committed_transactions);
404 self.discarded_transactions.extend(other.discarded_transactions);
405 self.stale_transactions.extend(other.stale_transactions);
406 }
407
408 pub fn committed_transactions(&self) -> &[TransactionUpdate] {
410 &self.committed_transactions
411 }
412
413 pub fn discarded_transactions(&self) -> &[TransactionId] {
415 &self.discarded_transactions
416 }
417
418 pub fn insert_discarded_transaction(&mut self, transaction_id: TransactionId) {
420 self.discarded_transactions.push(transaction_id);
421 }
422
423 pub fn stale_transactions(&self) -> &[TransactionRecord] {
425 &self.stale_transactions
426 }
427}
428
429impl Client {
431 pub async fn get_transactions(
436 &self,
437 filter: TransactionFilter,
438 ) -> Result<Vec<TransactionRecord>, ClientError> {
439 self.store.get_transactions(filter).await.map_err(Into::into)
440 }
441
442 pub async fn new_transaction(
459 &mut self,
460 account_id: AccountId,
461 transaction_request: TransactionRequest,
462 ) -> Result<TransactionResult, ClientError> {
463 self.validate_request(account_id, &transaction_request).await?;
465
466 let authenticated_input_note_ids: Vec<NoteId> =
470 transaction_request.authenticated_input_note_ids().collect::<Vec<_>>();
471
472 let authenticated_note_records = self
473 .store
474 .get_input_notes(NoteFilter::List(authenticated_input_note_ids))
475 .await?;
476
477 for authenticated_note_record in authenticated_note_records {
478 if !authenticated_note_record.is_authenticated() {
479 return Err(ClientError::TransactionRequestError(
480 TransactionRequestError::InputNoteNotAuthenticated,
481 ));
482 }
483 }
484
485 let unauthenticated_input_notes = transaction_request
487 .unauthenticated_input_notes()
488 .iter()
489 .cloned()
490 .map(Into::into)
491 .collect::<Vec<_>>();
492
493 self.store.upsert_input_notes(&unauthenticated_input_notes).await?;
494
495 let note_ids = transaction_request.get_input_note_ids();
496
497 let output_notes: Vec<Note> =
498 transaction_request.expected_output_notes().cloned().collect();
499
500 let future_notes: Vec<(NoteDetails, NoteTag)> =
501 transaction_request.expected_future_notes().cloned().collect();
502
503 let tx_script = transaction_request.build_transaction_script(
504 &self.get_account_interface(account_id).await?,
505 self.in_debug_mode,
506 )?;
507
508 let foreign_accounts = transaction_request.foreign_accounts().clone();
509 let mut tx_args = transaction_request.into_transaction_args(tx_script);
510
511 let fpi_block_num =
513 self.inject_foreign_account_inputs(foreign_accounts, &mut tx_args).await?;
514
515 let block_num = if let Some(block_num) = fpi_block_num {
516 block_num
517 } else {
518 self.store.get_sync_height().await?
519 };
520
521 let executed_transaction = self
523 .tx_executor
524 .execute_transaction(account_id, block_num, ¬e_ids, tx_args)
525 .await?;
526
527 let tx_note_auth_commitments: BTreeSet<Digest> =
533 notes_from_output(executed_transaction.output_notes())
534 .map(Note::commitment)
535 .collect();
536
537 let missing_note_ids: Vec<NoteId> = output_notes
538 .iter()
539 .filter_map(|n| (!tx_note_auth_commitments.contains(&n.commitment())).then_some(n.id()))
540 .collect();
541
542 if !missing_note_ids.is_empty() {
543 return Err(ClientError::MissingOutputNotes(missing_note_ids));
544 }
545
546 let screener = NoteScreener::new(self.store.clone());
547
548 TransactionResult::new(
549 executed_transaction,
550 screener,
551 future_notes,
552 self.get_sync_height().await?,
553 self.store.get_current_timestamp(),
554 )
555 .await
556 }
557
558 pub async fn submit_transaction(
561 &mut self,
562 tx_result: TransactionResult,
563 ) -> Result<(), ClientError> {
564 self.submit_transaction_with_prover(tx_result, self.tx_prover.clone()).await
565 }
566
567 pub async fn submit_transaction_with_prover(
570 &mut self,
571 tx_result: TransactionResult,
572 tx_prover: Arc<dyn TransactionProver>,
573 ) -> Result<(), ClientError> {
574 let proven_transaction = self.prove_transaction(&tx_result, tx_prover).await?;
575 self.submit_proven_transaction(proven_transaction).await?;
576 self.apply_transaction(tx_result).await
577 }
578
579 async fn prove_transaction(
581 &mut self,
582 tx_result: &TransactionResult,
583 tx_prover: Arc<dyn TransactionProver>,
584 ) -> Result<ProvenTransaction, ClientError> {
585 info!("Proving transaction...");
586
587 let proven_transaction =
588 tx_prover.prove(tx_result.executed_transaction().clone().into()).await?;
589
590 info!("Transaction proven.");
591
592 Ok(proven_transaction)
593 }
594
595 async fn submit_proven_transaction(
596 &mut self,
597 proven_transaction: ProvenTransaction,
598 ) -> Result<(), ClientError> {
599 info!("Submitting transaction to the network...");
600 self.rpc_api.submit_proven_transaction(proven_transaction).await?;
601 info!("Transaction submitted.");
602
603 Ok(())
604 }
605
606 async fn apply_transaction(&self, tx_result: TransactionResult) -> Result<(), ClientError> {
607 let transaction_id = tx_result.executed_transaction().id();
608 let sync_height = self.get_sync_height().await?;
609
610 info!("Applying transaction to the local store...");
613
614 let account_id = tx_result.executed_transaction().account_id();
615 let account_delta = tx_result.account_delta();
616 let account_record = self.try_get_account(account_id).await?;
617
618 if account_record.is_locked() {
619 return Err(ClientError::AccountLocked(account_id));
620 }
621
622 let mut account: Account = account_record.into();
623 account.apply_delta(account_delta)?;
624
625 if self
626 .store
627 .get_account_header_by_commitment(account.commitment())
628 .await?
629 .is_some()
630 {
631 return Err(ClientError::StoreError(StoreError::AccountCommitmentAlreadyExists(
632 account.commitment(),
633 )));
634 }
635
636 let created_input_notes = tx_result.relevant_notes().to_vec();
638 let new_tags = created_input_notes
639 .iter()
640 .filter_map(|note| {
641 if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) =
642 note.state()
643 {
644 Some(NoteTagRecord::with_note_source(*tag, note.id()))
645 } else {
646 None
647 }
648 })
649 .collect();
650
651 let created_output_notes = tx_result
653 .created_notes()
654 .iter()
655 .cloned()
656 .filter_map(|output_note| {
657 OutputNoteRecord::try_from_output_note(output_note, sync_height).ok()
658 })
659 .collect::<Vec<_>>();
660
661 let consumed_note_ids = tx_result.consumed_notes().iter().map(InputNote::id).collect();
662 let consumed_notes = self.get_input_notes(NoteFilter::List(consumed_note_ids)).await?;
663
664 let mut updated_input_notes = vec![];
665 for mut input_note_record in consumed_notes {
666 if input_note_record.consumed_locally(
667 account_id,
668 transaction_id,
669 self.store.get_current_timestamp(),
670 )? {
671 updated_input_notes.push(input_note_record);
672 }
673 }
674
675 let tx_update = TransactionStoreUpdate::new(
676 tx_result.into(),
677 account,
678 created_input_notes,
679 created_output_notes,
680 updated_input_notes,
681 new_tags,
682 );
683
684 self.store.apply_transaction(tx_update).await?;
685 info!("Transaction stored.");
686 Ok(())
687 }
688
689 pub fn compile_tx_script<T>(
691 &self,
692 inputs: T,
693 program: &str,
694 ) -> Result<TransactionScript, ClientError>
695 where
696 T: IntoIterator<Item = (Word, Vec<Felt>)>,
697 {
698 let assembler = TransactionKernel::assembler().with_debug_mode(self.in_debug_mode);
699 TransactionScript::compile(program, inputs, assembler)
700 .map_err(ClientError::TransactionScriptError)
701 }
702
703 fn get_outgoing_assets(
711 transaction_request: &TransactionRequest,
712 ) -> (BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>) {
713 let mut own_notes_assets = match transaction_request.script_template() {
715 Some(TransactionScriptTemplate::SendNotes(notes)) => {
716 notes.iter().map(|note| (note.id(), note.assets())).collect::<BTreeMap<_, _>>()
717 },
718 _ => BTreeMap::default(),
719 };
720 let mut output_notes_assets = transaction_request
722 .expected_output_notes()
723 .map(|note| (note.id(), note.assets()))
724 .collect::<BTreeMap<_, _>>();
725
726 output_notes_assets.append(&mut own_notes_assets);
728
729 let outgoing_assets =
731 output_notes_assets.values().flat_map(|note_assets| note_assets.iter());
732
733 collect_assets(outgoing_assets)
734 }
735
736 async fn get_incoming_assets(
738 &self,
739 transaction_request: &TransactionRequest,
740 ) -> Result<(BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>), TransactionRequestError>
741 {
742 let incoming_notes_ids: Vec<_> = transaction_request
744 .input_notes()
745 .iter()
746 .filter_map(|(note_id, _)| {
747 if transaction_request
748 .unauthenticated_input_notes()
749 .iter()
750 .any(|note| note.id() == *note_id)
751 {
752 None
753 } else {
754 Some(*note_id)
755 }
756 })
757 .collect();
758
759 let store_input_notes = self
760 .get_input_notes(NoteFilter::List(incoming_notes_ids))
761 .await
762 .map_err(|err| TransactionRequestError::NoteNotFound(err.to_string()))?;
763
764 let all_incoming_assets =
765 store_input_notes.iter().flat_map(|note| note.assets().iter()).chain(
766 transaction_request
767 .unauthenticated_input_notes()
768 .iter()
769 .flat_map(|note| note.assets().iter()),
770 );
771
772 Ok(collect_assets(all_incoming_assets))
773 }
774
775 async fn validate_basic_account_request(
776 &self,
777 transaction_request: &TransactionRequest,
778 account: &Account,
779 ) -> Result<(), ClientError> {
780 let (fungible_balance_map, non_fungible_set) =
782 Client::get_outgoing_assets(transaction_request);
783
784 let (incoming_fungible_balance_map, incoming_non_fungible_balance_set) =
786 self.get_incoming_assets(transaction_request).await?;
787
788 for (faucet_id, amount) in fungible_balance_map {
791 let account_asset_amount = account.vault().get_balance(faucet_id).unwrap_or(0);
792 let incoming_balance = incoming_fungible_balance_map.get(&faucet_id).unwrap_or(&0);
793 if account_asset_amount + incoming_balance < amount {
794 return Err(ClientError::AssetError(
795 AssetError::FungibleAssetAmountNotSufficient {
796 minuend: account_asset_amount,
797 subtrahend: amount,
798 },
799 ));
800 }
801 }
802
803 for non_fungible in non_fungible_set {
806 match account.vault().has_non_fungible_asset(non_fungible) {
807 Ok(true) => (),
808 Ok(false) => {
809 if !incoming_non_fungible_balance_set.contains(&non_fungible) {
811 return Err(ClientError::AssetError(
812 AssetError::NonFungibleFaucetIdTypeMismatch(
813 non_fungible.faucet_id_prefix(),
814 ),
815 ));
816 }
817 },
818 _ => {
819 return Err(ClientError::AssetError(
820 AssetError::NonFungibleFaucetIdTypeMismatch(
821 non_fungible.faucet_id_prefix(),
822 ),
823 ));
824 },
825 }
826 }
827
828 Ok(())
829 }
830
831 pub async fn validate_request(
838 &mut self,
839 account_id: AccountId,
840 transaction_request: &TransactionRequest,
841 ) -> Result<(), ClientError> {
842 let current_chain_tip =
843 self.rpc_api.get_block_header_by_number(None, false).await?.0.block_num();
844
845 if current_chain_tip > self.store.get_sync_height().await? + MAX_BLOCK_NUMBER_DELTA {
846 return Err(ClientError::RecencyConditionError(
847 "The client is too far behind the chain tip to execute the transaction".to_string(),
848 ));
849 }
850
851 let account: Account = self.try_get_account(account_id).await?.into();
852
853 if account.is_faucet() {
854 Ok(())
856 } else {
857 self.validate_basic_account_request(transaction_request, &account).await
858 }
859 }
860
861 async fn get_account_interface(
863 &mut self,
864 account_id: AccountId,
865 ) -> Result<AccountInterface, ClientError> {
866 let account: Account = self.try_get_account(account_id).await?.into();
867
868 Ok(AccountInterface::from(&account))
869 }
870
871 async fn inject_foreign_account_inputs(
885 &mut self,
886 foreign_accounts: BTreeSet<ForeignAccount>,
887 tx_args: &mut TransactionArgs,
888 ) -> Result<Option<BlockNumber>, ClientError> {
889 if foreign_accounts.is_empty() {
890 return Ok(None);
891 }
892
893 let account_ids = foreign_accounts.iter().map(ForeignAccount::account_id);
894 let known_account_codes =
895 self.store.get_foreign_account_code(account_ids.collect()).await?;
896
897 let known_account_codes: Vec<AccountCode> = known_account_codes.into_values().collect();
898
899 let (block_num, account_proofs) =
901 self.rpc_api.get_account_proofs(&foreign_accounts, known_account_codes).await?;
902
903 let mut account_proofs: BTreeMap<AccountId, AccountProof> =
904 account_proofs.into_iter().map(|proof| (proof.account_id(), proof)).collect();
905
906 for foreign_account in &foreign_accounts {
907 let (foreign_account_inputs, merkle_path) = match foreign_account {
908 ForeignAccount::Public(account_id, ..) => {
909 let account_proof = account_proofs
910 .remove(account_id)
911 .expect("Proof was requested and received");
912
913 let (foreign_account_inputs, merkle_path) = account_proof.try_into()?;
914
915 self.store
917 .upsert_foreign_account_code(
918 *account_id,
919 foreign_account_inputs.account_code().clone(),
920 )
921 .await?;
922
923 (foreign_account_inputs, merkle_path)
924 },
925 ForeignAccount::Private(foreign_account_inputs) => {
926 let account_id = foreign_account_inputs.account_header().id();
927 let proof = account_proofs
928 .remove(&account_id)
929 .expect("Proof was requested and received");
930 let merkle_path = proof.merkle_proof();
931
932 (foreign_account_inputs.clone(), merkle_path.clone())
933 },
934 };
935
936 extend_advice_inputs_for_foreign_account(
937 tx_args,
938 &mut self.tx_executor,
939 foreign_account_inputs,
940 &merkle_path,
941 )?;
942 }
943
944 if self.store.get_block_headers(&[block_num]).await?.is_empty() {
946 info!(
947 "Getting current block header data to execute transaction with foreign account requirements"
948 );
949 let summary = self.sync_state().await?;
950
951 if summary.block_num != block_num {
952 let mut current_partial_mmr = self.build_current_partial_mmr(true).await?;
953 self.get_and_store_authenticated_block(block_num, &mut current_partial_mmr)
954 .await?;
955 }
956 }
957
958 Ok(Some(block_num))
959 }
960
961 pub async fn execute_program(
966 &mut self,
967 account_id: AccountId,
968 tx_script: TransactionScript,
969 advice_inputs: AdviceInputs,
970 foreign_accounts: BTreeSet<ForeignAccount>,
971 ) -> Result<[Felt; 16], ClientError> {
972 let block_ref = self.get_sync_height().await?;
973
974 let mut tx_args =
975 TransactionArgs::with_tx_script(tx_script).with_advice_inputs(advice_inputs);
976 self.inject_foreign_account_inputs(foreign_accounts, &mut tx_args).await?;
977
978 Ok(self
979 .tx_executor
980 .execute_tx_view_script(
981 account_id,
982 block_ref,
983 tx_args.tx_script().expect("Transaction script should be present").clone(),
984 tx_args.advice_inputs().clone(),
985 )
986 .await?)
987 }
988}
989
990#[cfg(feature = "testing")]
994impl Client {
995 pub async fn testing_prove_transaction(
996 &mut self,
997 tx_result: &TransactionResult,
998 ) -> Result<ProvenTransaction, ClientError> {
999 self.prove_transaction(tx_result, self.tx_prover.clone()).await
1000 }
1001
1002 pub async fn testing_submit_proven_transaction(
1003 &mut self,
1004 proven_transaction: ProvenTransaction,
1005 ) -> Result<(), ClientError> {
1006 self.submit_proven_transaction(proven_transaction).await
1007 }
1008
1009 pub async fn testing_apply_transaction(
1010 &self,
1011 tx_result: TransactionResult,
1012 ) -> Result<(), ClientError> {
1013 self.apply_transaction(tx_result).await
1014 }
1015}
1016
1017fn extend_advice_inputs_for_foreign_account(
1020 tx_args: &mut TransactionArgs,
1021 tx_executor: &mut TransactionExecutor,
1022 foreign_account_inputs: ForeignAccountInputs,
1023 merkle_path: &MerklePath,
1024) -> Result<(), ClientError> {
1025 let (account_header, storage_header, account_code, proofs) =
1026 foreign_account_inputs.into_parts();
1027
1028 let account_id = account_header.id();
1029 let foreign_id_root =
1030 Digest::from([account_id.suffix(), account_id.prefix().as_felt(), ZERO, ZERO]);
1031
1032 tx_args.extend_advice_map([
1034 (foreign_id_root, account_header.as_elements()),
1036 (account_header.storage_commitment(), storage_header.as_elements()),
1038 (account_header.code_commitment(), account_code.as_elements()),
1040 ]);
1041
1042 for proof in proofs {
1044 tx_args.extend_merkle_store(
1046 proof.path().inner_nodes(proof.leaf().index().value(), proof.leaf().hash())?,
1047 );
1048 tx_args
1050 .extend_advice_map(core::iter::once((proof.leaf().hash(), proof.leaf().to_elements())));
1051 }
1052
1053 tx_args.extend_merkle_store(
1055 merkle_path.inner_nodes(account_id.prefix().as_u64(), account_header.commitment())?,
1056 );
1057
1058 tx_executor.load_account_code(&account_code);
1059
1060 Ok(())
1061}
1062
1063fn collect_assets<'a>(
1067 assets: impl Iterator<Item = &'a Asset>,
1068) -> (BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>) {
1069 let mut fungible_balance_map = BTreeMap::new();
1070 let mut non_fungible_set = BTreeSet::new();
1071
1072 assets.for_each(|asset| match asset {
1073 Asset::Fungible(fungible) => {
1074 fungible_balance_map
1075 .entry(fungible.faucet_id())
1076 .and_modify(|balance| *balance += fungible.amount())
1077 .or_insert(fungible.amount());
1078 },
1079 Asset::NonFungible(non_fungible) => {
1080 non_fungible_set.insert(*non_fungible);
1081 },
1082 });
1083
1084 (fungible_balance_map, non_fungible_set)
1085}
1086
1087pub fn notes_from_output(output_notes: &OutputNotes) -> impl Iterator<Item = &Note> {
1092 output_notes
1093 .iter()
1094 .filter(|n| matches!(n, OutputNote::Full(_)))
1095 .map(|n| match n {
1096 OutputNote::Full(n) => n,
1097 OutputNote::Header(_) | OutputNote::Partial(_) => {
1100 todo!("For now, all details should be held in OutputNote::Fulls")
1101 },
1102 })
1103}
1104
1105#[cfg(test)]
1106mod test {
1107 use miden_lib::{account::auth::RpoFalcon512, transaction::TransactionKernel};
1108 use miden_objects::{
1109 Word,
1110 account::{AccountBuilder, AccountComponent, AuthSecretKey, StorageMap, StorageSlot},
1111 asset::{Asset, FungibleAsset},
1112 crypto::dsa::rpo_falcon512::SecretKey,
1113 note::NoteType,
1114 testing::{
1115 account_component::BASIC_WALLET_CODE,
1116 account_id::{
1117 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
1118 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
1119 },
1120 },
1121 };
1122 use miden_tx::utils::{Deserializable, Serializable};
1123
1124 use super::PaymentTransactionData;
1125 use crate::{
1126 mock::create_test_client,
1127 transaction::{TransactionRequestBuilder, TransactionResult},
1128 };
1129
1130 #[tokio::test]
1131 async fn test_transaction_creates_two_notes() {
1132 let (mut client, _, keystore) = create_test_client().await;
1133 let asset_1: Asset =
1134 FungibleAsset::new(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into().unwrap(), 123)
1135 .unwrap()
1136 .into();
1137 let asset_2: Asset =
1138 FungibleAsset::new(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap(), 500)
1139 .unwrap()
1140 .into();
1141
1142 let secret_key = SecretKey::new();
1143 let pub_key = secret_key.public_key();
1144 keystore.add_key(&AuthSecretKey::RpoFalcon512(secret_key)).unwrap();
1145
1146 let wallet_component = AccountComponent::compile(
1147 BASIC_WALLET_CODE,
1148 TransactionKernel::assembler(),
1149 vec![StorageSlot::Value(Word::default()), StorageSlot::Map(StorageMap::default())],
1150 )
1151 .unwrap()
1152 .with_supports_all_types();
1153
1154 let anchor_block = client.get_latest_epoch_block().await.unwrap();
1155
1156 let account = AccountBuilder::new(Default::default())
1157 .anchor((&anchor_block).try_into().unwrap())
1158 .with_component(wallet_component)
1159 .with_component(RpoFalcon512::new(pub_key))
1160 .with_assets([asset_1, asset_2])
1161 .build_existing()
1162 .unwrap();
1163
1164 client.add_account(&account, None, false).await.unwrap();
1165 client.sync_state().await.unwrap();
1166 let tx_request = TransactionRequestBuilder::pay_to_id(
1167 PaymentTransactionData::new(
1168 vec![asset_1, asset_2],
1169 account.id(),
1170 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into().unwrap(),
1171 ),
1172 None,
1173 NoteType::Private,
1174 client.rng(),
1175 )
1176 .unwrap()
1177 .build()
1178 .unwrap();
1179
1180 let tx_result = client.new_transaction(account.id(), tx_request).await.unwrap();
1181 assert!(
1182 tx_result
1183 .created_notes()
1184 .get_note(0)
1185 .assets()
1186 .is_some_and(|assets| assets.num_assets() == 2)
1187 );
1188 client.testing_apply_transaction(tx_result.clone()).await.unwrap();
1190
1191 let bytes: std::vec::Vec<u8> = tx_result.to_bytes();
1193 let decoded = TransactionResult::read_from_bytes(&bytes).unwrap();
1194
1195 assert_eq!(tx_result, decoded);
1196 }
1197}