1use alloc::{
67 collections::{BTreeMap, BTreeSet},
68 string::{String, ToString},
69 sync::Arc,
70 vec::Vec,
71};
72use core::fmt::{self};
73
74pub use miden_lib::transaction::TransactionKernel;
75use miden_objects::{
76 account::{Account, AccountCode, AccountDelta, AccountId, AccountType},
77 asset::{Asset, NonFungibleAsset},
78 block::BlockNumber,
79 crypto::merkle::MerklePath,
80 note::{Note, NoteDetails, NoteId, NoteTag},
81 transaction::{InputNotes, TransactionArgs},
82 AssetError, Digest, Felt, Word, ZERO,
83};
84use miden_tx::{
85 utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
86 TransactionExecutor,
87};
88pub use miden_tx::{
89 LocalTransactionProver, ProvingOptions, TransactionProver, TransactionProverError,
90};
91use script_builder::{AccountCapabilities, AccountInterface};
92use tracing::info;
93
94use super::{Client, FeltRng};
95use crate::{
96 note::{NoteScreener, NoteUpdates},
97 rpc::domain::account::AccountProof,
98 store::{
99 input_note_states::ExpectedNoteState, InputNoteRecord, InputNoteState, NoteFilter,
100 OutputNoteRecord, StoreError, TransactionFilter,
101 },
102 sync::NoteTagRecord,
103 ClientError,
104};
105
106mod request;
107pub use request::{
108 ForeignAccount, ForeignAccountInputs, NoteArgs, PaymentTransactionData, SwapTransactionData,
109 TransactionRequest, TransactionRequestBuilder, TransactionRequestError,
110 TransactionScriptTemplate,
111};
112
113mod script_builder;
114pub use miden_objects::transaction::{
115 ExecutedTransaction, InputNote, OutputNote, OutputNotes, ProvenTransaction, TransactionId,
116 TransactionScript,
117};
118pub use miden_tx::{DataStoreError, TransactionExecutorError};
119pub use script_builder::TransactionScriptBuilderError;
120
121#[derive(Clone, Debug, PartialEq)]
131pub struct TransactionResult {
132 transaction: ExecutedTransaction,
133 relevant_notes: Vec<InputNoteRecord>,
134}
135
136impl TransactionResult {
137 pub async fn new(
140 transaction: ExecutedTransaction,
141 note_screener: NoteScreener,
142 partial_notes: Vec<(NoteDetails, NoteTag)>,
143 current_block_num: BlockNumber,
144 current_timestamp: Option<u64>,
145 ) -> Result<Self, ClientError> {
146 let mut relevant_notes = vec![];
147
148 for note in notes_from_output(transaction.output_notes()) {
149 let account_relevance = note_screener.check_relevance(note).await?;
150
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,
344 created_output_notes,
345 updated_input_notes,
346 vec![],
347 ),
348 new_tags,
349 }
350 }
351
352 pub fn executed_transaction(&self) -> &ExecutedTransaction {
354 &self.executed_transaction
355 }
356
357 pub fn updated_account(&self) -> &Account {
359 &self.updated_account
360 }
361
362 pub fn note_updates(&self) -> &NoteUpdates {
364 &self.note_updates
365 }
366
367 pub fn new_tags(&self) -> &[NoteTagRecord] {
369 &self.new_tags
370 }
371}
372
373impl<R: FeltRng> Client<R> {
375 pub async fn get_transactions(
380 &self,
381 filter: TransactionFilter,
382 ) -> Result<Vec<TransactionRecord>, ClientError> {
383 self.store.get_transactions(filter).await.map_err(|err| err.into())
384 }
385
386 pub async fn new_transaction(
403 &mut self,
404 account_id: AccountId,
405 transaction_request: TransactionRequest,
406 ) -> Result<TransactionResult, ClientError> {
407 self.validate_request(account_id, &transaction_request).await?;
409
410 let authenticated_input_note_ids: Vec<NoteId> =
414 transaction_request.authenticated_input_note_ids().collect::<Vec<_>>();
415
416 let authenticated_note_records = self
417 .store
418 .get_input_notes(NoteFilter::List(authenticated_input_note_ids))
419 .await?;
420
421 for authenticated_note_record in authenticated_note_records {
422 if !authenticated_note_record.is_authenticated() {
423 return Err(ClientError::TransactionRequestError(
424 TransactionRequestError::InputNoteNotAuthenticated,
425 ));
426 }
427 }
428
429 let unauthenticated_input_notes = transaction_request
431 .unauthenticated_input_notes()
432 .iter()
433 .cloned()
434 .map(|note| note.into())
435 .collect::<Vec<_>>();
436
437 self.store.upsert_input_notes(&unauthenticated_input_notes).await?;
438
439 let note_ids = transaction_request.get_input_note_ids();
440
441 let output_notes: Vec<Note> =
442 transaction_request.expected_output_notes().cloned().collect();
443
444 let future_notes: Vec<(NoteDetails, NoteTag)> =
445 transaction_request.expected_future_notes().cloned().collect();
446
447 let tx_script = transaction_request.build_transaction_script(
448 self.get_account_capabilities(account_id).await?,
449 self.in_debug_mode,
450 )?;
451
452 let foreign_accounts = transaction_request.foreign_accounts().clone();
453 let mut tx_args = transaction_request.into_transaction_args(tx_script);
454
455 let fpi_block_num =
457 self.inject_foreign_account_inputs(foreign_accounts, &mut tx_args).await?;
458
459 let block_num = if let Some(block_num) = fpi_block_num {
460 block_num
461 } else {
462 self.store.get_sync_height().await?
463 };
464
465 let executed_transaction = self
467 .tx_executor
468 .execute_transaction(account_id, block_num, ¬e_ids, tx_args)
469 .await?;
470
471 let tx_note_auth_hashes: BTreeSet<Digest> =
477 notes_from_output(executed_transaction.output_notes())
478 .map(|note| note.hash())
479 .collect();
480
481 let missing_note_ids: Vec<NoteId> = output_notes
482 .iter()
483 .filter_map(|n| (!tx_note_auth_hashes.contains(&n.hash())).then_some(n.id()))
484 .collect();
485
486 if !missing_note_ids.is_empty() {
487 return Err(ClientError::MissingOutputNotes(missing_note_ids));
488 }
489
490 let screener = NoteScreener::new(self.store.clone());
491
492 TransactionResult::new(
493 executed_transaction,
494 screener,
495 future_notes,
496 self.get_sync_height().await?,
497 self.store.get_current_timestamp(),
498 )
499 .await
500 }
501
502 pub async fn submit_transaction(
505 &mut self,
506 tx_result: TransactionResult,
507 ) -> Result<(), ClientError> {
508 self.submit_transaction_with_prover(tx_result, self.tx_prover.clone()).await
509 }
510
511 pub async fn submit_transaction_with_prover(
514 &mut self,
515 tx_result: TransactionResult,
516 tx_prover: Arc<dyn TransactionProver>,
517 ) -> Result<(), ClientError> {
518 let proven_transaction = self.prove_transaction(&tx_result, tx_prover).await?;
519 self.submit_proven_transaction(proven_transaction).await?;
520 self.apply_transaction(tx_result).await
521 }
522
523 async fn prove_transaction(
525 &mut self,
526 tx_result: &TransactionResult,
527 tx_prover: Arc<dyn TransactionProver>,
528 ) -> Result<ProvenTransaction, ClientError> {
529 info!("Proving transaction...");
530
531 let proven_transaction =
532 tx_prover.prove(tx_result.executed_transaction().clone().into()).await?;
533
534 info!("Transaction proven.");
535
536 Ok(proven_transaction)
537 }
538
539 async fn submit_proven_transaction(
540 &mut self,
541 proven_transaction: ProvenTransaction,
542 ) -> Result<(), ClientError> {
543 info!("Submitting transaction to the network...");
544 self.rpc_api.submit_proven_transaction(proven_transaction).await?;
545 info!("Transaction submitted.");
546
547 Ok(())
548 }
549
550 async fn apply_transaction(&self, tx_result: TransactionResult) -> Result<(), ClientError> {
551 let transaction_id = tx_result.executed_transaction().id();
552 let sync_height = self.get_sync_height().await?;
553
554 info!("Applying transaction to the local store...");
557
558 let account_id = tx_result.executed_transaction().account_id();
559 let account_delta = tx_result.account_delta();
560 let account_record = self.try_get_account(account_id).await?;
561
562 if account_record.is_locked() {
563 return Err(ClientError::AccountLocked(account_id));
564 }
565
566 let mut account: Account = account_record.into();
567 account.apply_delta(account_delta)?;
568
569 if self.store.get_account_header_by_hash(account.hash()).await?.is_some() {
570 return Err(ClientError::StoreError(StoreError::AccountHashAlreadyExists(
571 account.hash(),
572 )));
573 }
574
575 let created_input_notes = tx_result.relevant_notes().to_vec();
577 let new_tags = created_input_notes
578 .iter()
579 .filter_map(|note| {
580 if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) =
581 note.state()
582 {
583 Some(NoteTagRecord::with_note_source(*tag, note.id()))
584 } else {
585 None
586 }
587 })
588 .collect();
589
590 let created_output_notes = tx_result
592 .created_notes()
593 .iter()
594 .cloned()
595 .filter_map(|output_note| {
596 OutputNoteRecord::try_from_output_note(output_note, sync_height).ok()
597 })
598 .collect::<Vec<_>>();
599
600 let consumed_note_ids = tx_result.consumed_notes().iter().map(|note| note.id()).collect();
601 let consumed_notes = self.get_input_notes(NoteFilter::List(consumed_note_ids)).await?;
602
603 let mut updated_input_notes = vec![];
604 for mut input_note_record in consumed_notes {
605 if input_note_record.consumed_locally(
606 account_id,
607 transaction_id,
608 self.store.get_current_timestamp(),
609 )? {
610 updated_input_notes.push(input_note_record);
611 }
612 }
613
614 let tx_update = TransactionStoreUpdate::new(
615 tx_result.into(),
616 account,
617 created_input_notes,
618 created_output_notes,
619 updated_input_notes,
620 new_tags,
621 );
622
623 self.store.apply_transaction(tx_update).await?;
624 info!("Transaction stored.");
625 Ok(())
626 }
627
628 pub fn compile_tx_script<T>(
630 &self,
631 inputs: T,
632 program: &str,
633 ) -> Result<TransactionScript, ClientError>
634 where
635 T: IntoIterator<Item = (Word, Vec<Felt>)>,
636 {
637 let assembler = TransactionKernel::assembler().with_debug_mode(self.in_debug_mode);
638 TransactionScript::compile(program, inputs, assembler)
639 .map_err(ClientError::TransactionScriptError)
640 }
641
642 fn get_outgoing_assets(
650 &self,
651 transaction_request: &TransactionRequest,
652 ) -> (BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>) {
653 let mut own_notes_assets = match transaction_request.script_template() {
655 Some(TransactionScriptTemplate::SendNotes(notes)) => {
656 notes.iter().map(|note| (note.id(), note.assets())).collect::<BTreeMap<_, _>>()
657 },
658 _ => Default::default(),
659 };
660 let mut output_notes_assets = transaction_request
662 .expected_output_notes()
663 .map(|note| (note.id(), note.assets()))
664 .collect::<BTreeMap<_, _>>();
665
666 output_notes_assets.append(&mut own_notes_assets);
668
669 let outgoing_assets =
671 output_notes_assets.values().flat_map(|note_assets| note_assets.iter());
672
673 collect_assets(outgoing_assets)
674 }
675
676 async fn get_incoming_assets(
678 &self,
679 transaction_request: &TransactionRequest,
680 ) -> Result<(BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>), TransactionRequestError>
681 {
682 let incoming_notes_ids: Vec<_> = transaction_request
684 .input_notes()
685 .iter()
686 .filter_map(|(note_id, _)| {
687 if transaction_request
688 .unauthenticated_input_notes()
689 .iter()
690 .any(|note| note.id() == *note_id)
691 {
692 None
693 } else {
694 Some(*note_id)
695 }
696 })
697 .collect();
698
699 let store_input_notes = self
700 .get_input_notes(NoteFilter::List(incoming_notes_ids))
701 .await
702 .map_err(|err| TransactionRequestError::NoteNotFound(err.to_string()))?;
703
704 let all_incoming_assets =
705 store_input_notes.iter().flat_map(|note| note.assets().iter()).chain(
706 transaction_request
707 .unauthenticated_input_notes()
708 .iter()
709 .flat_map(|note| note.assets().iter()),
710 );
711
712 Ok(collect_assets(all_incoming_assets))
713 }
714
715 async fn validate_basic_account_request(
716 &self,
717 transaction_request: &TransactionRequest,
718 account: &Account,
719 ) -> Result<(), ClientError> {
720 let (fungible_balance_map, non_fungible_set) =
722 self.get_outgoing_assets(transaction_request);
723
724 let (incoming_fungible_balance_map, incoming_non_fungible_balance_set) =
726 self.get_incoming_assets(transaction_request).await?;
727
728 for (faucet_id, amount) in fungible_balance_map {
731 let account_asset_amount = account.vault().get_balance(faucet_id).unwrap_or(0);
732 let incoming_balance = incoming_fungible_balance_map.get(&faucet_id).unwrap_or(&0);
733 if account_asset_amount + incoming_balance < amount {
734 return Err(ClientError::AssetError(
735 AssetError::FungibleAssetAmountNotSufficient {
736 minuend: account_asset_amount,
737 subtrahend: amount,
738 },
739 ));
740 }
741 }
742
743 for non_fungible in non_fungible_set {
746 match account.vault().has_non_fungible_asset(non_fungible) {
747 Ok(true) => (),
748 Ok(false) => {
749 if !incoming_non_fungible_balance_set.contains(&non_fungible) {
751 return Err(ClientError::AssetError(
752 AssetError::NonFungibleFaucetIdTypeMismatch(
753 non_fungible.faucet_id_prefix(),
754 ),
755 ));
756 }
757 },
758 _ => {
759 return Err(ClientError::AssetError(
760 AssetError::NonFungibleFaucetIdTypeMismatch(
761 non_fungible.faucet_id_prefix(),
762 ),
763 ));
764 },
765 }
766 }
767
768 Ok(())
769 }
770
771 pub async fn validate_request(
777 &self,
778 account_id: AccountId,
779 transaction_request: &TransactionRequest,
780 ) -> Result<(), ClientError> {
781 let account: Account = self.try_get_account(account_id).await?.into();
782
783 if account.is_faucet() {
784 Ok(())
786 } else {
787 self.validate_basic_account_request(transaction_request, &account).await
788 }
789 }
790
791 async fn get_account_capabilities(
793 &self,
794 account_id: AccountId,
795 ) -> Result<AccountCapabilities, ClientError> {
796 let account: Account = self.try_get_account(account_id).await?.into();
797 let account_auth = self.try_get_account_auth(account_id).await?;
798
799 let account_capabilities = match account.account_type() {
801 AccountType::FungibleFaucet => AccountInterface::BasicFungibleFaucet,
802 AccountType::NonFungibleFaucet => todo!("Non fungible faucet not supported yet"),
803 AccountType::RegularAccountImmutableCode | AccountType::RegularAccountUpdatableCode => {
804 AccountInterface::BasicWallet
805 },
806 };
807
808 Ok(AccountCapabilities {
809 account_id,
810 auth: account_auth,
811 interfaces: account_capabilities,
812 })
813 }
814
815 async fn inject_foreign_account_inputs(
825 &mut self,
826 foreign_accounts: BTreeSet<ForeignAccount>,
827 tx_args: &mut TransactionArgs,
828 ) -> Result<Option<BlockNumber>, ClientError> {
829 if foreign_accounts.is_empty() {
830 return Ok(None);
831 }
832
833 let account_ids = foreign_accounts.iter().map(|acc| acc.account_id());
834 let known_account_codes =
835 self.store.get_foreign_account_code(account_ids.collect()).await?;
836
837 let known_account_codes: Vec<AccountCode> = known_account_codes.into_values().collect();
838
839 let (block_num, account_proofs) =
841 self.rpc_api.get_account_proofs(&foreign_accounts, known_account_codes).await?;
842
843 let mut account_proofs: BTreeMap<AccountId, AccountProof> =
844 account_proofs.into_iter().map(|proof| (proof.account_id(), proof)).collect();
845
846 for foreign_account in foreign_accounts.iter() {
847 let (foreign_account_inputs, merkle_path) = match foreign_account {
848 ForeignAccount::Public(account_id, ..) => {
849 let account_proof = account_proofs
850 .remove(account_id)
851 .expect("Proof was requested and received");
852
853 let (foreign_account_inputs, merkle_path) = account_proof.try_into()?;
854
855 self.store
857 .upsert_foreign_account_code(
858 *account_id,
859 foreign_account_inputs.account_code().clone(),
860 )
861 .await?;
862
863 (foreign_account_inputs, merkle_path)
864 },
865 ForeignAccount::Private(foreign_account_inputs) => {
866 let account_id = foreign_account_inputs.account_header().id();
867 let proof = account_proofs
868 .remove(&account_id)
869 .expect("Proof was requested and received");
870 let merkle_path = proof.merkle_proof();
871
872 (foreign_account_inputs.clone(), merkle_path.clone())
873 },
874 };
875
876 extend_advice_inputs_for_foreign_account(
877 tx_args,
878 &mut self.tx_executor,
879 foreign_account_inputs,
880 &merkle_path,
881 )?;
882 }
883
884 if self.store.get_block_headers(&[block_num]).await?.is_empty() {
886 info!("Getting current block header data to execute transaction with foreign account requirements");
887 let summary = self.sync_state().await?;
888
889 if summary.block_num != block_num {
890 let mut current_partial_mmr = self.build_current_partial_mmr(true).await?;
891 self.get_and_store_authenticated_block(block_num, &mut current_partial_mmr)
892 .await?;
893 }
894 }
895
896 Ok(Some(block_num))
897 }
898}
899
900#[cfg(feature = "testing")]
904impl<R: FeltRng> Client<R> {
905 pub async fn testing_prove_transaction(
906 &mut self,
907 tx_result: &TransactionResult,
908 ) -> Result<ProvenTransaction, ClientError> {
909 self.prove_transaction(tx_result, self.tx_prover.clone()).await
910 }
911
912 pub async fn testing_submit_proven_transaction(
913 &mut self,
914 proven_transaction: ProvenTransaction,
915 ) -> Result<(), ClientError> {
916 self.submit_proven_transaction(proven_transaction).await
917 }
918
919 pub async fn testing_apply_transaction(
920 &self,
921 tx_result: TransactionResult,
922 ) -> Result<(), ClientError> {
923 self.apply_transaction(tx_result).await
924 }
925}
926
927fn extend_advice_inputs_for_foreign_account(
930 tx_args: &mut TransactionArgs,
931 tx_executor: &mut TransactionExecutor,
932 foreign_account_inputs: ForeignAccountInputs,
933 merkle_path: &MerklePath,
934) -> Result<(), ClientError> {
935 let (account_header, storage_header, account_code, proofs) =
936 foreign_account_inputs.into_parts();
937
938 let account_id = account_header.id();
939 let foreign_id_root =
940 Digest::from([account_id.suffix(), account_id.prefix().as_felt(), ZERO, ZERO]);
941
942 tx_args.extend_advice_map([
944 (foreign_id_root, account_header.as_elements()),
946 (account_header.storage_commitment(), storage_header.as_elements()),
948 (account_header.code_commitment(), account_code.as_elements()),
950 ]);
951
952 for proof in proofs {
954 tx_args.extend_merkle_store(
956 proof.path().inner_nodes(proof.leaf().index().value(), proof.leaf().hash())?,
957 );
958 tx_args
960 .extend_advice_map(core::iter::once((proof.leaf().hash(), proof.leaf().to_elements())));
961 }
962
963 tx_args.extend_merkle_store(
965 merkle_path.inner_nodes(account_id.prefix().as_u64(), account_header.hash())?,
966 );
967
968 tx_executor.load_account_code(&account_code);
969
970 Ok(())
971}
972
973fn collect_assets<'a>(
977 assets: impl Iterator<Item = &'a Asset>,
978) -> (BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>) {
979 let mut fungible_balance_map = BTreeMap::new();
980 let mut non_fungible_set = BTreeSet::new();
981
982 assets.for_each(|asset| match asset {
983 Asset::Fungible(fungible) => {
984 fungible_balance_map
985 .entry(fungible.faucet_id())
986 .and_modify(|balance| *balance += fungible.amount())
987 .or_insert(fungible.amount());
988 },
989 Asset::NonFungible(non_fungible) => {
990 non_fungible_set.insert(*non_fungible);
991 },
992 });
993
994 (fungible_balance_map, non_fungible_set)
995}
996
997pub(crate) fn prepare_word(word: &Word) -> String {
998 word.iter().map(|x| x.as_int().to_string()).collect::<Vec<_>>().join(".")
999}
1000
1001pub fn notes_from_output(output_notes: &OutputNotes) -> impl Iterator<Item = &Note> {
1006 output_notes
1007 .iter()
1008 .filter(|n| matches!(n, OutputNote::Full(_)))
1009 .map(|n| match n {
1010 OutputNote::Full(n) => n,
1011 OutputNote::Header(_) | OutputNote::Partial(_) => {
1014 todo!("For now, all details should be held in OutputNote::Fulls")
1015 },
1016 })
1017}
1018
1019#[cfg(test)]
1020mod test {
1021 use miden_lib::{account::auth::RpoFalcon512, transaction::TransactionKernel};
1022 use miden_objects::{
1023 account::{AccountBuilder, AccountComponent, StorageMap, StorageSlot},
1024 asset::{Asset, FungibleAsset},
1025 crypto::dsa::rpo_falcon512::SecretKey,
1026 note::NoteType,
1027 testing::{
1028 account_component::BASIC_WALLET_CODE,
1029 account_id::{
1030 ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN,
1031 ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN,
1032 },
1033 },
1034 Word,
1035 };
1036 use miden_tx::utils::{Deserializable, Serializable};
1037
1038 use super::PaymentTransactionData;
1039 use crate::{
1040 mock::create_test_client,
1041 transaction::{TransactionRequestBuilder, TransactionResult},
1042 };
1043
1044 #[tokio::test]
1045 async fn test_transaction_creates_two_notes() {
1046 let (mut client, _) = create_test_client().await;
1047 let asset_1: Asset =
1048 FungibleAsset::new(ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN.try_into().unwrap(), 123)
1049 .unwrap()
1050 .into();
1051 let asset_2: Asset =
1052 FungibleAsset::new(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN.try_into().unwrap(), 500)
1053 .unwrap()
1054 .into();
1055
1056 let secret_key = SecretKey::new();
1057
1058 let wallet_component = AccountComponent::compile(
1059 BASIC_WALLET_CODE,
1060 TransactionKernel::assembler(),
1061 vec![StorageSlot::Value(Word::default()), StorageSlot::Map(StorageMap::default())],
1062 )
1063 .unwrap()
1064 .with_supports_all_types();
1065
1066 let anchor_block = client.get_latest_epoch_block().await.unwrap();
1067
1068 let account = AccountBuilder::new(Default::default())
1069 .anchor((&anchor_block).try_into().unwrap())
1070 .with_component(wallet_component)
1071 .with_component(RpoFalcon512::new(secret_key.public_key()))
1072 .with_assets([asset_1, asset_2])
1073 .build_existing()
1074 .unwrap();
1075
1076 client
1077 .add_account(
1078 &account,
1079 None,
1080 &miden_objects::account::AuthSecretKey::RpoFalcon512(secret_key.clone()),
1081 false,
1082 )
1083 .await
1084 .unwrap();
1085 client.sync_state().await.unwrap();
1086 let tx_request = TransactionRequestBuilder::pay_to_id(
1087 PaymentTransactionData::new(
1088 vec![asset_1, asset_2],
1089 account.id(),
1090 ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN.try_into().unwrap(),
1091 ),
1092 None,
1093 NoteType::Private,
1094 client.rng(),
1095 )
1096 .unwrap()
1097 .build();
1098
1099 let tx_result = client.new_transaction(account.id(), tx_request).await.unwrap();
1100 assert!(tx_result
1101 .created_notes()
1102 .get_note(0)
1103 .assets()
1104 .is_some_and(|assets| assets.num_assets() == 2));
1105 client.testing_apply_transaction(tx_result.clone()).await.unwrap();
1107
1108 let bytes: std::vec::Vec<u8> = tx_result.to_bytes();
1110 let decoded = TransactionResult::read_from_bytes(&bytes).unwrap();
1111
1112 assert_eq!(tx_result, decoded);
1113 }
1114}