1use alloc::boxed::Box;
66use alloc::collections::{BTreeMap, BTreeSet};
67use alloc::string::String;
68use alloc::sync::Arc;
69use alloc::vec::Vec;
70
71use miden_protocol::account::{AccountCode, AccountCodeInterface, AccountId, PartialAccount};
72use miden_protocol::asset::{Asset, NonFungibleAsset};
73use miden_protocol::block::{BlockHeader, BlockNumber, FeeParameters};
74use miden_protocol::errors::AssetError;
75use miden_protocol::note::{
76 Note,
77 NoteAttachments,
78 NoteDetails,
79 NoteId,
80 NoteRecipient,
81 NoteScript,
82 NoteTag,
83};
84use miden_protocol::transaction::PartialBlockchain;
85use miden_protocol::vm::MIN_STACK_DEPTH;
86use miden_protocol::{Felt, Word};
87use miden_standards::account::auth::FeeConversionInfo;
88use miden_standards::account::faucets::FungibleFaucet;
89use miden_standards::account::interface::AccountComponentInterfaceExt;
90use miden_standards::note::TxFeeNote;
91use miden_tx::{DataStore, NoteConsumptionChecker, TransactionExecutor};
92use tracing::info;
93
94use super::Client;
95use crate::ClientError;
96use crate::note::{NoteScreenerError, NoteUpdateTracker, StandardNote};
97use crate::rpc::domain::account::{
98 AccountStorageRequirements,
99 GetAccountRequest,
100 StorageMapFetch,
101 VaultFetch,
102};
103use crate::rpc::encryption::{TransactionEncryptionKey, seal_transaction_inputs};
104use crate::rpc::{AccountStateAt, NodeRpcClient, RpcError};
105use crate::store::data_store::{ClientDataStore, build_partial_mmr_with_paths};
106use crate::store::input_note_states::ExpectedNoteState;
107use crate::store::{
108 AccountRecord,
109 InputNoteRecord,
110 InputNoteState,
111 NoteFilter,
112 NoteRecordError,
113 OutputNoteRecord,
114 Store,
115 StoreError,
116 TransactionFilter,
117};
118use crate::sync::NoteTagRecord;
119use crate::transaction::batch::InMemoryBatchDataStore;
120
121pub mod batch;
122pub use batch::{BatchBuilder, BatchBuilderError};
123
124mod chain_anchor;
125pub use chain_anchor::{ChainAnchor, ChainAnchorError};
126
127#[cfg(feature = "dap")]
128mod dap_executor;
129mod prover;
130pub use prover::TransactionProver;
131
132mod record;
133pub use record::{
134 DiscardCause,
135 TransactionDetails,
136 TransactionRecord,
137 TransactionStatus,
138 TransactionStatusVariant,
139};
140
141mod store_update;
142pub use store_update::TransactionStoreUpdate;
143
144mod request;
145pub use request::{
146 ForeignAccount,
147 NoteArgs,
148 PaymentNoteDescription,
149 PswapTransactionData,
150 SwapTransactionData,
151 TransactionRequest,
152 TransactionRequestBuilder,
153 TransactionRequestError,
154 TransactionScriptTemplate,
155 build_fpi_script,
156};
157
158mod observer;
159pub use observer::TransactionObserver;
160
161mod result;
162pub use miden_protocol::transaction::{
165 AccountInputs,
166 ExecutedTransaction,
167 InputNote,
168 InputNotes,
169 OutputNote,
170 OutputNotes,
171 ProvenTransaction,
172 PublicOutputNote,
173 RawOutputNote,
174 RawOutputNotes,
175 TransactionArgs,
176 TransactionId,
177 TransactionInputs,
178 TransactionKernel,
179 TransactionScript,
180 TransactionScriptRoot,
181 TransactionSummary,
182};
183pub use miden_protocol::vm::{AdviceInputs, AdviceMap};
184pub use miden_standards::account::interface::{AccountComponentInterface, AccountInterface};
185pub use miden_standards::tx_script::{
186 ExpirationTransactionScript,
187 SendNotesTransactionScriptError,
188};
189pub use miden_tx::auth::TransactionAuthenticator;
190pub use miden_tx::{
191 DataStoreError,
192 LocalTransactionProver,
193 ProvingOptions,
194 TransactionExecutorError,
195 TransactionProverError,
196};
197pub use result::TransactionResult;
198
199pub(crate) const NATIVE_FEE_CONVERSION_SALT: Word = Word::empty();
206
207impl<AUTH> Client<AUTH>
209where
210 AUTH: TransactionAuthenticator + Sync + 'static,
211{
212 pub async fn get_transactions(
217 &self,
218 filter: TransactionFilter,
219 ) -> Result<Vec<TransactionRecord>, ClientError> {
220 self.store.get_transactions(filter).await.map_err(Into::into)
221 }
222
223 pub fn new_transaction_batch(&mut self) -> BatchBuilder<'_, AUTH> {
231 let inner_data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
232 BatchBuilder {
233 client: self,
234 data_store: InMemoryBatchDataStore::new(inner_data_store),
235 pushed_txs: Vec::new(),
236 consumed_input_notes: BTreeSet::new(),
237 }
238 }
239
240 pub async fn submit_new_transaction(
249 &mut self,
250 account_id: AccountId,
251 transaction_request: TransactionRequest,
252 ) -> Result<TransactionId, ClientError> {
253 let prover = self.tx_prover.clone();
254 self.submit_new_transaction_with_prover(account_id, transaction_request, prover)
255 .await
256 }
257
258 pub async fn submit_new_transaction_with_prover(
265 &mut self,
266 account_id: AccountId,
267 transaction_request: TransactionRequest,
268 tx_prover: Arc<dyn TransactionProver>,
269 ) -> Result<TransactionId, ClientError> {
270 if !transaction_request.expected_ntx_scripts().is_empty() {
273 Box::pin(self.ensure_ntx_scripts_registered(
274 account_id,
275 transaction_request.expected_ntx_scripts(),
276 tx_prover.clone(),
277 ))
278 .await?;
279 }
280
281 let tx_result = self.execute_transaction(account_id, transaction_request).await?;
282 let tx_id = tx_result.executed_transaction().id();
283
284 let proven_transaction = self.prove_transaction_with(&tx_result, tx_prover).await?;
285 let submission_height =
286 self.submit_proven_transaction(proven_transaction, &tx_result).await?;
287
288 let tx_update =
297 Box::new(self.get_transaction_store_update(&tx_result, submission_height).await?);
298
299 if let Err(apply_err) = self.apply_transaction_update((*tx_update).clone()).await {
300 info!(
301 "apply_transaction_update failed for submitted tx {tx_id}; returning \
302 ApplyTransactionAfterSubmitFailed with the pending update attached: {apply_err}"
303 );
304 return Err(ClientError::ApplyTransactionAfterSubmitFailed {
305 pending_update: tx_update,
306 source: Box::new(apply_err),
307 });
308 }
309
310 for observer in &self.transaction_observers {
314 crate::errors::log_observer_failure(
315 observer.name(),
316 "TransactionObserver::apply",
317 observer.apply(&tx_result).await,
318 );
319 }
320
321 Ok(tx_id)
322 }
323
324 pub async fn execute_transaction(
334 &self,
335 account_id: AccountId,
336 transaction_request: TransactionRequest,
337 ) -> Result<TransactionResult, ClientError> {
338 self.execute_transaction_with_mode(
339 account_id,
340 transaction_request,
341 TransactionExecutionMode::Standard,
342 None,
343 )
344 .await
345 }
346
347 pub async fn execute_transaction_at(
381 &mut self,
382 account_id: AccountId,
383 transaction_request: TransactionRequest,
384 anchor: ChainAnchor,
385 ) -> Result<TransactionResult, ClientError> {
386 let result = self
387 .execute_transaction_with_mode(
388 account_id,
389 transaction_request,
390 TransactionExecutionMode::Standard,
391 Some(Box::new(anchor)),
392 )
393 .await?;
394
395 let expiration = result.executed_transaction().expiration_block_num();
400 let sync_height = self.store.get_sync_height().await?;
401 if expiration <= sync_height {
402 return Err(
403 ChainAnchorError::AnchoredTransactionExpired { expiration, sync_height }.into()
404 );
405 }
406
407 Ok(result)
408 }
409
410 async fn chain_anchor_at_tip(
415 &self,
416 tracked_blocks: BTreeSet<BlockNumber>,
417 ) -> Result<ChainAnchor, ClientError> {
418 let sync_height = self.store.get_sync_height().await?;
419
420 let (header, _had_notes) = self
421 .store
422 .get_block_header_by_num(sync_height)
423 .await?
424 .ok_or(StoreError::BlockHeaderNotFound(sync_height))?;
425
426 let mut tracked_blocks = tracked_blocks;
427 tracked_blocks.remove(&sync_height);
429
430 let block_headers: Vec<BlockHeader> = self
431 .store
432 .get_block_headers(&tracked_blocks)
433 .await?
434 .into_iter()
435 .map(|(header, _has_notes)| header)
436 .collect();
437
438 let fetched_nums: BTreeSet<BlockNumber> =
441 block_headers.iter().map(BlockHeader::block_num).collect();
442 if let Some(&missing) = tracked_blocks.difference(&fetched_nums).next() {
443 return Err(StoreError::BlockHeaderNotFound(missing).into());
444 }
445
446 let peaks = self.store.get_current_blockchain_peaks().await?;
447 let partial_mmr = build_partial_mmr_with_paths(&self.store, peaks, &block_headers).await?;
448
449 let chain = PartialBlockchain::new(partial_mmr, block_headers)?;
450
451 Ok(ChainAnchor::new(header, chain)?)
452 }
453
454 pub async fn chain_anchor_for_request(
473 &self,
474 transaction_request: &TransactionRequest,
475 ) -> Result<ChainAnchor, ClientError> {
476 let inferred_input_note_ids: Vec<NoteId> = transaction_request
477 .input_note_ids()
478 .filter(|note_id| !transaction_request.explicit_input_notes.contains_key(note_id))
479 .collect();
480
481 let mut tracked_blocks: BTreeSet<BlockNumber> = if inferred_input_note_ids.is_empty() {
482 BTreeSet::new()
483 } else {
484 self.store
485 .get_input_notes(NoteFilter::List(inferred_input_note_ids))
486 .await?
487 .iter()
488 .filter(|record| record.is_authenticated())
489 .filter_map(|record| record.inclusion_proof())
490 .map(|proof| proof.location().block_num())
491 .collect()
492 };
493 tracked_blocks.extend(
494 transaction_request
495 .explicit_input_notes
496 .values()
497 .filter_map(InputNote::proof)
498 .map(|proof| proof.location().block_num()),
499 );
500
501 self.chain_anchor_at_tip(tracked_blocks).await
502 }
503
504 #[cfg(feature = "dap")]
518 pub async fn execute_transaction_with_dap(
519 &self,
520 account_id: AccountId,
521 transaction_request: TransactionRequest,
522 ) -> Result<TransactionResult, ClientError> {
523 self.execute_transaction_with_mode(
524 account_id,
525 transaction_request,
526 TransactionExecutionMode::Dap,
527 None,
528 )
529 .await
530 }
531
532 async fn execute_transaction_with_mode(
536 &self,
537 account_id: AccountId,
538 transaction_request: TransactionRequest,
539 execution_mode: TransactionExecutionMode,
540 anchor: Option<Box<ChainAnchor>>,
541 ) -> Result<TransactionResult, ClientError> {
542 let account: PartialAccount =
543 self.get_native_account_record(account_id).await?.try_into()?;
544
545 let prep = self
546 .prepare_transaction(&account, transaction_request, anchor.as_deref())
547 .await?;
548
549 let mut data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
550 if let Some(anchor) = anchor {
551 data_store = data_store.with_chain_anchor(*anchor);
552 }
553 data_store.register_note_scripts(prep.output_note_scripts());
554 for fpi_account in &prep.foreign_account_inputs {
555 data_store.mast_store().load_account_code(fpi_account.code());
556 }
557 data_store.register_foreign_account_inputs(prep.foreign_account_inputs);
558
559 data_store.mast_store().load_account_code(account.code());
560
561 let mut notes = prep.notes;
562 if prep.ignore_invalid_notes {
563 notes = self
564 .get_valid_input_notes(
565 &data_store,
566 account.id(),
567 prep.block_num,
568 notes,
569 prep.tx_args.clone(),
570 )
571 .await?;
572 }
573
574 let executed_transaction = match execution_mode {
575 TransactionExecutionMode::Standard => {
576 self.build_executor(&data_store)?
577 .execute_transaction(account_id, prep.block_num, notes, prep.tx_args)
578 .await?
579 },
580 #[cfg(feature = "dap")]
581 TransactionExecutionMode::Dap => {
582 self.build_dap_executor(&data_store)?
583 .execute_transaction(account_id, prep.block_num, notes, prep.tx_args)
584 .await?
585 },
586 };
587
588 validate_executed_transaction(&executed_transaction, &prep.output_recipients)?;
589 TransactionResult::new(executed_transaction, prep.future_notes)
590 }
591
592 pub(crate) async fn prepare_transaction(
608 &self,
609 account: &PartialAccount,
610 transaction_request: TransactionRequest,
611 anchor: Option<&ChainAnchor>,
612 ) -> Result<PreparedTransaction, ClientError> {
613 self.validate_account_request(
614 &transaction_request,
615 account.id(),
616 &account.code_interface(),
617 )
618 .await?;
619
620 self.prepare_transaction_inner(account.code_interface(), transaction_request, anchor)
621 .await
622 }
623
624 pub(crate) async fn prepare_transaction_for_batch(
625 &self,
626 account: &PartialAccount,
627 transaction_request: TransactionRequest,
628 ) -> Result<PreparedTransaction, ClientError> {
629 self.prepare_transaction_inner(account.code_interface(), transaction_request, None)
630 .await
631 }
632
633 async fn prepare_transaction_inner(
634 &self,
635 account_code_interface: AccountCodeInterface,
636 mut transaction_request: TransactionRequest,
637 anchor: Option<&ChainAnchor>,
638 ) -> Result<PreparedTransaction, ClientError> {
639 if anchor.is_none() {
640 self.validate_recency().await?;
641 }
642
643 let mut stored_note_records = self
645 .store
646 .get_input_notes(NoteFilter::List(transaction_request.input_note_ids().collect()))
647 .await?;
648
649 for note in &stored_note_records {
651 if note.is_consumed() {
652 return Err(ClientError::TransactionRequestError(
653 TransactionRequestError::InputNoteAlreadyConsumed(note.details_commitment()),
654 ));
655 }
656 }
657
658 stored_note_records.retain(InputNoteRecord::is_authenticated);
660
661 let notes = transaction_request.build_input_notes(stored_note_records)?;
662
663 if let Some(anchor) = anchor {
667 for note in notes.iter() {
668 if let Some(location) = note.location() {
669 let block_num = location.block_num();
670 if block_num < anchor.block_num()
671 && !anchor.partial_blockchain().contains_block(block_num)
672 {
673 return Err(ChainAnchorError::BlockNotTracked { block_num }.into());
674 }
675 }
676 }
677 }
678
679 let output_recipients =
680 transaction_request.expected_output_recipients().cloned().collect::<Vec<_>>();
681
682 let future_notes: Vec<(NoteDetails, NoteTag)> =
683 transaction_request.expected_future_notes().cloned().collect();
684
685 let tx_script = transaction_request.build_transaction_script(&account_code_interface)?;
686
687 let foreign_accounts = transaction_request.foreign_accounts().clone();
688
689 let block_num = match anchor {
692 Some(anchor) => anchor.block_num(),
693 None => self.store.get_sync_height().await?,
694 };
695
696 let foreign_account_inputs = self
697 .get_foreign_account_inputs(foreign_accounts.into_values(), block_num)
698 .await?;
699
700 let ignore_invalid_notes = transaction_request.ignore_invalid_input_notes();
701
702 let reference_header = match anchor {
703 Some(anchor) => anchor.header().clone(),
704 None => {
705 self.store
706 .get_block_header_by_num(block_num)
707 .await?
708 .ok_or(StoreError::BlockHeaderNotFound(block_num))?
709 .0
710 },
711 };
712
713 for inputs in &foreign_account_inputs {
716 if inputs.compute_account_root().ok() != Some(reference_header.account_root()) {
717 return Err(TransactionRequestError::ForeignAccountNotAtReferenceBlock {
718 account_id: inputs.id(),
719 block_num,
720 }
721 .into());
722 }
723 }
724
725 attach_native_fee_conversion_info(
726 &mut transaction_request,
727 &account_code_interface,
728 &reference_header,
729 )?;
730
731 let tx_args = transaction_request.into_transaction_args(tx_script);
732
733 Ok(PreparedTransaction {
734 notes,
735 output_recipients,
736 future_notes,
737 tx_args,
738 foreign_account_inputs,
739 block_num,
740 ignore_invalid_notes,
741 })
742 }
743
744 pub async fn prove_transaction(
746 &self,
747 tx_result: &TransactionResult,
748 ) -> Result<ProvenTransaction, ClientError> {
749 self.prove_transaction_with(tx_result, self.tx_prover.clone()).await
750 }
751
752 pub async fn prove_transaction_with(
760 &self,
761 tx_result: &TransactionResult,
762 tx_prover: Arc<dyn TransactionProver>,
763 ) -> Result<ProvenTransaction, ClientError> {
764 info!("Proving transaction...");
765
766 let executed_transaction = tx_result.executed_transaction();
767 let proven_transaction = tx_prover.prove(executed_transaction.clone().into()).await?;
768
769 if proven_transaction.id() != executed_transaction.id() {
778 return Err(ClientError::MismatchedProvenTransaction {
779 requested: executed_transaction.id(),
780 returned: proven_transaction.id(),
781 });
782 }
783
784 info!("Transaction proven.");
785
786 Ok(proven_transaction)
787 }
788
789 pub async fn submit_proven_transaction(
799 &mut self,
800 proven_transaction: ProvenTransaction,
801 transaction_inputs: impl Into<TransactionInputs>,
802 ) -> Result<BlockNumber, ClientError> {
803 info!("Submitting transaction to the network...");
804 let tx_id = proven_transaction.id();
805 let key = self.transaction_encryption_key().await?;
806
807 let transaction_inputs = transaction_inputs.into();
811 let submitted = proven_transaction.clone();
812
813 let sealed_inputs =
814 seal_transaction_inputs(&mut self.rng, &key, tx_id, &transaction_inputs)?;
815
816 let result =
817 self.rpc_api.submit_proven_transaction(proven_transaction, sealed_inputs).await;
818 if let Err(err) = &result {
819 self.forget_stale_transaction_encryption_key(err).await;
820 }
821
822 let block_num = result
823 .map_err(|err| promote_indeterminate_submission(err, submitted, transaction_inputs))?;
824 info!("Transaction submitted.");
825
826 Ok(block_num)
827 }
828
829 pub(crate) async fn transaction_encryption_key(
837 &self,
838 ) -> Result<TransactionEncryptionKey, ClientError> {
839 if let Some(key) = self.store.get_transaction_encryption_key().await? {
840 return Ok(key);
841 }
842
843 let attested = self.rpc_api.get_transaction_encryption_key().await?;
844
845 let genesis_commitment =
849 self.trusted_block_header(BlockNumber::GENESIS).await?.commitment();
850 let chain_tip = self.store.get_sync_height().await?;
851 let validator_keys = self.trusted_block_header(chain_tip).await?.validator_keys().clone();
852
853 let key = attested.verify(genesis_commitment, &validator_keys)?;
854 self.store.set_transaction_encryption_key(&key).await?;
855
856 Ok(key)
857 }
858
859 #[cfg(feature = "testing")]
862 pub async fn seed_transaction_encryption_key(
863 &self,
864 key: TransactionEncryptionKey,
865 ) -> Result<(), ClientError> {
866 Ok(self.store.set_transaction_encryption_key(&key).await?)
867 }
868
869 pub(crate) async fn forget_stale_transaction_encryption_key(&self, err: &RpcError) {
875 if err.is_stale_transaction_encryption_key()
876 && let Err(err) = self.store.remove_transaction_encryption_key().await
877 {
878 tracing::warn!("failed to evict the stale transaction encryption key: {err}");
879 }
880 }
881
882 async fn trusted_block_header(
889 &self,
890 block_num: BlockNumber,
891 ) -> Result<BlockHeader, ClientError> {
892 self.store.get_block_header_by_num(block_num).await?.map(|(header, _)| header).ok_or_else(
893 || {
894 ClientError::ChainValidationError(alloc::format!(
895 "block header {block_num} is not tracked locally; sync the client before it can verify data against the chain"
896 ))
897 },
898 )
899 }
900
901 pub async fn get_transaction_store_update(
904 &self,
905 tx_result: &TransactionResult,
906 submission_height: BlockNumber,
907 ) -> Result<TransactionStoreUpdate, TransactionStoreUpdateError> {
908 let note_updates = self.get_note_updates(submission_height, tx_result).await?;
909
910 let new_tags: Vec<NoteTagRecord> = note_updates
913 .updated_input_notes()
914 .filter_map(|note| {
915 let note = note.inner();
916
917 if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) =
918 note.state()
919 {
920 Some(NoteTagRecord::with_note_source(*tag, note.details_commitment()))
921 } else {
922 None
923 }
924 })
925 .collect();
926
927 Ok(TransactionStoreUpdate::new(
928 tx_result.executed_transaction().clone(),
929 submission_height,
930 note_updates,
931 tx_result.future_notes().to_vec(),
932 new_tags,
933 ))
934 }
935
936 pub async fn apply_transaction(
939 &self,
940 tx_result: &TransactionResult,
941 submission_height: BlockNumber,
942 ) -> Result<(), ClientError> {
943 let tx_update = self.get_transaction_store_update(tx_result, submission_height).await?;
944
945 self.apply_transaction_update(tx_update).await?;
946
947 for observer in &self.transaction_observers {
949 if let Err(err) = observer.apply(tx_result).await {
950 tracing::warn!(
951 observer = observer.name(),
952 error = ?err,
953 "TransactionObserver::apply failed; continuing with remaining observers",
954 );
955 }
956 }
957
958 Ok(())
959 }
960
961 pub async fn apply_transaction_update(
962 &self,
963 tx_update: TransactionStoreUpdate,
964 ) -> Result<(), ClientError> {
965 info!("Applying transaction to the local store...");
968
969 let executed_transaction = tx_update.executed_transaction();
970 let account_id = executed_transaction.account_id();
971
972 if self.account_reader(account_id).status().await?.is_locked() {
973 return Err(ClientError::AccountLocked(account_id));
974 }
975
976 self.store.apply_transaction(tx_update).await?;
977 info!("Transaction stored.");
978 Ok(())
979 }
980
981 pub async fn execute_program(
986 &self,
987 account_id: AccountId,
988 tx_script: TransactionScript,
989 advice_inputs: AdviceInputs,
990 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
991 ) -> Result<[Felt; MIN_STACK_DEPTH], ClientError> {
992 let (data_store, block_ref) =
993 self.prepare_program_execution(account_id, foreign_accounts).await?;
994
995 Ok(self
996 .build_executor(&data_store)?
997 .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
998 .await?)
999 }
1000
1001 #[cfg(feature = "dap")]
1004 pub async fn execute_program_with_dap(
1005 &self,
1006 account_id: AccountId,
1007 tx_script: TransactionScript,
1008 advice_inputs: AdviceInputs,
1009 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
1010 ) -> Result<[Felt; MIN_STACK_DEPTH], ClientError> {
1011 let (data_store, block_ref) =
1012 self.prepare_program_execution(account_id, foreign_accounts).await?;
1013
1014 Ok(self
1015 .build_dap_executor(&data_store)?
1016 .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
1017 .await?)
1018 }
1019
1020 pub async fn validate_request(
1030 &self,
1031 account_id: AccountId,
1032 transaction_request: &TransactionRequest,
1033 ) -> Result<(), ClientError> {
1034 self.validate_recency().await?;
1035 validate_output_note_senders(transaction_request, account_id)?;
1036 let account: PartialAccount = self
1037 .store
1038 .get_minimal_partial_account(account_id)
1039 .await?
1040 .ok_or(ClientError::AccountDataNotFound(account_id))?
1041 .try_into()?;
1042 self.validate_account_request(transaction_request, account_id, &account.code_interface())
1043 .await
1044 }
1045
1046 async fn validate_account_request(
1051 &self,
1052 transaction_request: &TransactionRequest,
1053 account_id: AccountId,
1054 account_code_interface: &AccountCodeInterface,
1055 ) -> Result<(), ClientError> {
1056 validate_fee_conversion_info_support(transaction_request, account_code_interface)?;
1057
1058 if account_code_interface.contains([FungibleFaucet::mint_and_send_root()]) {
1059 Ok(())
1061 } else {
1062 let assets = self.account_reader(account_id).assets().await?;
1063 validate_basic_account_request(transaction_request, &assets)
1064 }
1065 }
1066
1067 async fn validate_recency(&self) -> Result<(), ClientError> {
1068 if let Some(max_block_number_delta) = self.max_block_number_delta {
1069 let current_chain_tip =
1070 self.rpc_api.get_block_header_by_number(None, false).await?.0.block_num();
1071
1072 if current_chain_tip > self.store.get_sync_height().await? + max_block_number_delta {
1073 return Err(ClientError::RecencyConditionError(
1074 "The client is too far behind the chain tip to execute the transaction",
1075 ));
1076 }
1077 }
1078 Ok(())
1079 }
1080
1081 pub async fn ensure_ntx_scripts_registered(
1094 &mut self,
1095 account_id: AccountId,
1096 scripts: &[NoteScript],
1097 tx_prover: Arc<dyn TransactionProver>,
1098 ) -> Result<(), ClientError> {
1099 let mut missing_scripts = Vec::new();
1100
1101 for script in scripts {
1102 if StandardNote::from_script(script).is_some() {
1104 continue;
1105 }
1106
1107 let script_root = script.root();
1108
1109 match self.rpc_api.get_note_script_by_root(script_root.into()).await {
1111 Ok(Some(_)) => {},
1112 Ok(None) => missing_scripts.push(script.clone()),
1113 Err(source) => {
1114 return Err(ClientError::NtxScriptRegistrationFailed {
1115 script_root: script_root.into(),
1116 source,
1117 });
1118 },
1119 }
1120 }
1121
1122 if missing_scripts.is_empty() {
1123 return Ok(());
1124 }
1125
1126 let registration_request = TransactionRequestBuilder::new().build_register_note_scripts(
1127 account_id,
1128 missing_scripts,
1129 self.rng(),
1130 )?;
1131
1132 let tx_result = self.execute_transaction(account_id, registration_request).await?;
1133 let proven = self.prove_transaction_with(&tx_result, tx_prover).await?;
1134 let submission_height = self.submit_proven_transaction(proven, &tx_result).await?;
1135 self.apply_transaction(&tx_result, submission_height).await?;
1136
1137 Ok(())
1138 }
1139
1140 pub(crate) async fn get_valid_input_notes<STORE: DataStore + Sync>(
1149 &self,
1150 data_store: &STORE,
1151 account_id: AccountId,
1152 block_ref: BlockNumber,
1153 mut input_notes: InputNotes<InputNote>,
1154 tx_args: TransactionArgs,
1155 ) -> Result<InputNotes<InputNote>, ClientError> {
1156 loop {
1157 if input_notes.is_empty() {
1160 break;
1161 }
1162
1163 let execution = NoteConsumptionChecker::new(&self.build_executor(data_store)?)
1164 .check_notes_consumability(
1165 account_id,
1166 block_ref,
1167 input_notes.iter().map(|n| n.clone().into_note()).collect(),
1168 tx_args.clone(),
1169 )
1170 .await?;
1171
1172 if execution.failed().is_empty() {
1173 break;
1174 }
1175
1176 let failed_note_ids: BTreeSet<NoteId> =
1177 execution.failed().iter().map(|n| n.note().id()).collect();
1178 let filtered_input_notes = InputNotes::new(
1179 input_notes
1180 .into_iter()
1181 .filter(|note| !failed_note_ids.contains(¬e.id()))
1182 .collect(),
1183 )
1184 .expect("Created from a valid input notes list");
1185
1186 input_notes = filtered_input_notes;
1187 }
1188
1189 Ok(input_notes)
1190 }
1191
1192 pub async fn get_foreign_account_inputs(
1214 &self,
1215 foreign_accounts: impl IntoIterator<Item = ForeignAccount>,
1216 block_num: BlockNumber,
1217 ) -> Result<Vec<AccountInputs>, ClientError> {
1218 let foreign_accounts = foreign_accounts.into_iter();
1219 let mut return_foreign_account_inputs = Vec::with_capacity(foreign_accounts.size_hint().0);
1220
1221 for foreign_account in foreign_accounts {
1222 let foreign_account_inputs = match foreign_account {
1223 ForeignAccount::Public(account_id, storage_requirements) => {
1224 fetch_public_account_inputs(
1225 &self.store,
1226 &self.rpc_api,
1227 account_id,
1228 storage_requirements,
1229 AccountStateAt::Block(block_num),
1230 )
1231 .await?
1232 },
1233 ForeignAccount::Private(partial_account) => {
1234 let account_id = partial_account.id();
1235 let (_, account_proof) = self
1236 .rpc_api
1237 .get_account(
1238 account_id,
1239 GetAccountRequest::new().at(AccountStateAt::Block(block_num)),
1240 )
1241 .await?;
1242 let (witness, _) = account_proof.into_parts();
1243 AccountInputs::new(partial_account, witness)
1244 },
1245 ForeignAccount::Prefetched(inputs) => inputs,
1246 };
1247
1248 return_foreign_account_inputs.push(foreign_account_inputs);
1249 }
1250
1251 Ok(return_foreign_account_inputs)
1252 }
1253
1254 async fn prepare_program_execution(
1258 &self,
1259 account_id: AccountId,
1260 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
1261 ) -> Result<(ClientDataStore, BlockNumber), ClientError> {
1262 let block_ref = self.get_sync_height().await?;
1263
1264 let foreign_account_inputs = self
1265 .get_foreign_account_inputs(foreign_accounts.into_values(), block_ref)
1266 .await?;
1267
1268 let account_code = self
1269 .store
1270 .get_account_code(account_id)
1271 .await?
1272 .ok_or(ClientError::AccountDataNotFound(account_id))?;
1273
1274 let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
1275
1276 data_store.mast_store().load_account_code(&account_code);
1278
1279 for fpi_account in &foreign_account_inputs {
1280 data_store.mast_store().load_account_code(fpi_account.code());
1281 }
1282
1283 data_store.register_foreign_account_inputs(foreign_account_inputs);
1284
1285 Ok((data_store, block_ref))
1286 }
1287
1288 pub(crate) fn build_executor<'store, 'auth, STORE: DataStore + Sync>(
1291 &'auth self,
1292 data_store: &'store STORE,
1293 ) -> Result<TransactionExecutor<'store, 'auth, STORE, AUTH>, TransactionExecutorError> {
1294 let mut executor = TransactionExecutor::new(data_store)
1295 .with_options(self.exec_options)?
1296 .with_source_manager(self.source_manager.clone());
1297 if let Some(authenticator) = self.authenticator.as_deref() {
1298 executor = executor.with_authenticator(authenticator);
1299 }
1300 Ok(executor)
1301 }
1302
1303 async fn get_native_account_record(
1308 &self,
1309 account_id: AccountId,
1310 ) -> Result<AccountRecord, ClientError> {
1311 let account_record = self
1312 .store
1313 .get_minimal_partial_account(account_id)
1314 .await?
1315 .ok_or(ClientError::AccountDataNotFound(account_id))?;
1316 if account_record.is_watched() {
1317 return Err(ClientError::AccountIsWatched(account_id));
1318 }
1319 Ok(account_record)
1320 }
1321
1322 #[cfg(feature = "dap")]
1324 pub(crate) fn build_dap_executor<'store, 'auth, STORE: DataStore + Sync>(
1325 &'auth self,
1326 data_store: &'store STORE,
1327 ) -> Result<
1328 TransactionExecutor<'store, 'auth, STORE, AUTH, dap_executor::DapProgramExecutor>,
1329 TransactionExecutorError,
1330 > {
1331 Ok(self
1332 .build_executor(data_store)?
1333 .with_program_executor::<dap_executor::DapProgramExecutor>())
1334 }
1335
1336 async fn get_note_updates(
1339 &self,
1340 submission_height: BlockNumber,
1341 tx_result: &TransactionResult,
1342 ) -> Result<NoteUpdateTracker, TransactionStoreUpdateError> {
1343 let executed_tx = tx_result.executed_transaction();
1344 let current_timestamp = self.store.get_current_timestamp();
1345 let current_block_num = self.store.get_sync_height().await?;
1346
1347 let new_output_notes = executed_tx
1359 .output_notes()
1360 .iter()
1361 .filter(|output_note| {
1362 output_note
1363 .recipient()
1364 .is_none_or(|recipient| recipient.script().root() != TxFeeNote::script_root())
1365 })
1366 .cloned()
1367 .filter_map(|output_note| {
1368 OutputNoteRecord::try_from_output_note(output_note, submission_height).ok()
1369 })
1370 .collect::<Vec<_>>();
1371
1372 let mut new_input_notes = vec![];
1374 let output_notes: Vec<Note> =
1375 notes_from_output(executed_tx.output_notes()).cloned().collect();
1376 let note_screener = self.note_screener().clone();
1377 let output_note_relevances = note_screener.get_batch_consumability(&output_notes).await?;
1378
1379 for note in output_notes {
1380 if note.script().root() == TxFeeNote::script_root() {
1385 continue;
1386 }
1387
1388 if output_note_relevances.contains_key(¬e.id()) {
1389 let metadata = *note.metadata();
1390 let tag = metadata.tag();
1391 let attachments = note.attachments().clone();
1392
1393 new_input_notes.push(InputNoteRecord::new(
1394 note.into(),
1395 attachments,
1396 current_timestamp,
1397 ExpectedNoteState {
1398 metadata: Some(metadata),
1399 after_block_num: submission_height,
1400 tag: Some(tag),
1401 }
1402 .into(),
1403 ));
1404 }
1405 }
1406
1407 new_input_notes.extend(tx_result.future_notes().iter().map(|(note_details, tag)| {
1409 InputNoteRecord::new(
1410 note_details.clone(),
1411 NoteAttachments::empty(),
1412 None,
1413 ExpectedNoteState {
1414 metadata: None,
1415 after_block_num: current_block_num,
1416 tag: Some(*tag),
1417 }
1418 .into(),
1419 )
1420 }));
1421
1422 let consumed_note_ids =
1427 executed_tx.tx_inputs().input_notes().iter().map(InputNote::id).collect();
1428
1429 let consumed_notes =
1430 self.store.get_input_notes(NoteFilter::List(consumed_note_ids)).await?;
1431
1432 let tracked_note_ids =
1433 consumed_notes.iter().filter_map(InputNoteRecord::id).collect::<BTreeSet<_>>();
1434
1435 for input_note in executed_tx.tx_inputs().input_notes() {
1436 if !tracked_note_ids.contains(&input_note.id()) {
1437 let mut input_note_record = InputNoteRecord::from(input_note.clone());
1438 input_note_record.consumed_locally(
1439 executed_tx.account_id(),
1440 executed_tx.id(),
1441 current_timestamp,
1442 )?;
1443 new_input_notes.push(input_note_record);
1444 }
1445 }
1446
1447 let mut updated_input_notes = vec![];
1448
1449 for mut input_note_record in consumed_notes {
1450 if input_note_record.consumed_locally(
1451 executed_tx.account_id(),
1452 executed_tx.id(),
1453 current_timestamp,
1454 )? {
1455 updated_input_notes.push(input_note_record);
1456 }
1457 }
1458
1459 Ok(NoteUpdateTracker::for_transaction_updates(
1460 new_input_notes,
1461 updated_input_notes,
1462 new_output_notes,
1463 ))
1464 }
1465}
1466
1467#[derive(Debug, thiserror::Error)]
1473pub enum TransactionStoreUpdateError {
1474 #[error("store error")]
1475 Store(#[from] StoreError),
1476 #[error("note screener error")]
1477 NoteScreener(#[from] NoteScreenerError),
1478 #[error("note record error")]
1479 NoteRecord(#[from] NoteRecordError),
1480}
1481
1482#[derive(Clone, Copy, Debug)]
1486enum TransactionExecutionMode {
1487 Standard,
1488 #[cfg(feature = "dap")]
1489 Dap,
1490}
1491
1492pub(crate) struct PreparedTransaction {
1494 pub(crate) notes: InputNotes<InputNote>,
1495 pub(crate) output_recipients: Vec<NoteRecipient>,
1496 pub(crate) future_notes: Vec<(NoteDetails, NoteTag)>,
1497 pub(crate) tx_args: TransactionArgs,
1498 pub(crate) foreign_account_inputs: Vec<AccountInputs>,
1499 pub(crate) block_num: BlockNumber,
1500 pub(crate) ignore_invalid_notes: bool,
1501}
1502
1503impl PreparedTransaction {
1504 pub(crate) fn output_note_scripts(&self) -> impl Iterator<Item = NoteScript> + '_ {
1507 self.output_recipients.iter().map(|recipient| recipient.script().clone())
1508 }
1509}
1510
1511fn get_outgoing_assets(
1516 transaction_request: &TransactionRequest,
1517) -> (BTreeMap<AccountId, u64>, Vec<NonFungibleAsset>) {
1518 let mut own_notes_assets = match transaction_request.script_template() {
1520 Some(TransactionScriptTemplate::SendNotes(notes)) => notes
1521 .iter()
1522 .map(|note| (note.id(), note.assets().clone()))
1523 .collect::<BTreeMap<_, _>>(),
1524 _ => BTreeMap::default(),
1525 };
1526 let mut output_notes_assets = transaction_request
1528 .expected_output_own_notes()
1529 .into_iter()
1530 .map(|note| (note.id(), note.assets().clone()))
1531 .collect::<BTreeMap<_, _>>();
1532
1533 output_notes_assets.append(&mut own_notes_assets);
1535
1536 let outgoing_assets = output_notes_assets.values().flat_map(|note_assets| note_assets.iter());
1538
1539 request::collect_assets(outgoing_assets)
1540}
1541
1542fn attach_native_fee_conversion_info(
1557 transaction_request: &mut TransactionRequest,
1558 account_code_interface: &AccountCodeInterface,
1559 reference_header: &BlockHeader,
1560) -> Result<(), ClientError> {
1561 if transaction_request.has_auth_arg() {
1565 return Ok(());
1566 }
1567
1568 let fee_parameters = reference_header.fee_parameters();
1569 let declared_salt = transaction_request.fee_conversion_salt();
1570 if fee_parameters.verification_base_fee() == 0 && declared_salt.is_none() {
1571 return Ok(());
1572 }
1573
1574 match FeeAuth::of(account_code_interface) {
1575 FeeAuth::FixedSalt => {
1576 transaction_request.commit_native_fee_conversion_info(
1577 fee_parameters.fee_faucet_id(),
1578 declared_salt.unwrap_or(NATIVE_FEE_CONVERSION_SALT),
1579 );
1580 Ok(())
1581 },
1582 FeeAuth::CallerChosenSalt(component) => match declared_salt {
1583 Some(salt) => {
1584 transaction_request
1585 .commit_native_fee_conversion_info(fee_parameters.fee_faucet_id(), salt);
1586 Ok(())
1587 },
1588 None => Err(ClientError::TransactionRequestError(
1589 TransactionRequestError::FeeConversionInfoRequired(component),
1590 )),
1591 },
1592 FeeAuth::Ignored(component) => match declared_salt {
1593 Some(_) => Err(ClientError::TransactionRequestError(
1596 TransactionRequestError::FeeConversionInfoUnsupported(component),
1597 )),
1598 None => Ok(()),
1599 },
1600 }
1601}
1602
1603enum FeeAuth {
1605 FixedSalt,
1608 CallerChosenSalt(String),
1611 Ignored(String),
1615}
1616
1617impl FeeAuth {
1618 fn of(account_code_interface: &AccountCodeInterface) -> Self {
1627 let procedures: Vec<_> = account_code_interface.procedures().iter().copied().collect();
1628 let components = AccountComponentInterface::from_procedures(&procedures);
1629
1630 if components
1631 .iter()
1632 .any(|component| matches!(component, AccountComponentInterface::AuthSingleSig))
1633 {
1634 return Self::FixedSalt;
1635 }
1636
1637 let caller_chosen_salt = components.iter().find_map(|component| match component {
1642 AccountComponentInterface::AuthMultisig
1643 | AccountComponentInterface::AuthMultisigSmart
1644 | AccountComponentInterface::AuthGuardedMultisig => {
1645 Some(Self::CallerChosenSalt(component.name()))
1646 },
1647 _ => None,
1648 });
1649
1650 caller_chosen_salt.unwrap_or_else(|| {
1651 let name = components
1652 .iter()
1653 .find(|component| {
1654 matches!(
1655 component,
1656 AccountComponentInterface::AuthNoAuth
1657 | AccountComponentInterface::AuthNetworkAccount
1658 )
1659 })
1660 .map_or_else(|| "unrecognized".into(), AccountComponentInterface::name);
1661
1662 Self::Ignored(name)
1663 })
1664 }
1665}
1666
1667pub(crate) fn native_fee_conversion_info(
1672 account_code_interface: &AccountCodeInterface,
1673 fee_parameters: &FeeParameters,
1674) -> Option<FeeConversionInfo> {
1675 if fee_parameters.verification_base_fee() == 0 {
1676 return None;
1677 }
1678
1679 match FeeAuth::of(account_code_interface) {
1682 FeeAuth::FixedSalt => Some(FeeConversionInfo::one_to_one(fee_parameters.fee_faucet_id())),
1683 FeeAuth::CallerChosenSalt(_) | FeeAuth::Ignored(_) => None,
1684 }
1685}
1686
1687fn validate_fee_conversion_info_support(
1693 transaction_request: &TransactionRequest,
1694 account_code_interface: &AccountCodeInterface,
1695) -> Result<(), ClientError> {
1696 if transaction_request.fee_conversion_salt().is_none() {
1697 return Ok(());
1698 }
1699
1700 match FeeAuth::of(account_code_interface) {
1701 FeeAuth::FixedSalt | FeeAuth::CallerChosenSalt(_) => Ok(()),
1702 FeeAuth::Ignored(auth_component) => Err(ClientError::TransactionRequestError(
1703 TransactionRequestError::FeeConversionInfoUnsupported(auth_component),
1704 )),
1705 }
1706}
1707fn validate_output_note_senders(
1715 transaction_request: &TransactionRequest,
1716 account_id: AccountId,
1717) -> Result<(), ClientError> {
1718 for note in transaction_request.expected_output_own_notes() {
1719 let sender = note.metadata().sender();
1720 if sender != account_id {
1721 return Err(ClientError::TransactionRequestError(
1722 TransactionRequestError::OutputNoteSenderMismatch {
1723 expected: account_id,
1724 actual: sender,
1725 },
1726 ));
1727 }
1728 }
1729
1730 Ok(())
1731}
1732
1733fn validate_basic_account_request(
1736 transaction_request: &TransactionRequest,
1737 vault_assets: &[Asset],
1738) -> Result<(), ClientError> {
1739 let (fungible_balance_map, non_fungible_set) = get_outgoing_assets(transaction_request);
1741
1742 let (incoming_fungible_balance_map, incoming_non_fungible_balance_set) =
1744 transaction_request.incoming_assets();
1745
1746 let mut available_fungible: BTreeMap<AccountId, u64> = BTreeMap::new();
1749 for asset in vault_assets {
1750 if let Asset::Fungible(fungible) = asset {
1751 let balance = available_fungible.entry(fungible.faucet_id()).or_default();
1752 *balance = balance.saturating_add(fungible.amount().as_u64());
1753 }
1754 }
1755
1756 for (faucet_id, amount) in fungible_balance_map {
1759 let account_asset_amount = available_fungible.get(&faucet_id).copied().unwrap_or(0);
1760 let incoming_balance = incoming_fungible_balance_map.get(&faucet_id).unwrap_or(&0);
1761 if account_asset_amount + incoming_balance < amount {
1762 return Err(ClientError::AssetError(AssetError::FungibleAssetAmountNotSufficient {
1763 minuend: account_asset_amount,
1764 subtrahend: amount,
1765 }));
1766 }
1767 }
1768
1769 for non_fungible in &non_fungible_set {
1772 let held = vault_assets
1773 .iter()
1774 .any(|asset| matches!(asset, Asset::NonFungible(nf) if nf == non_fungible));
1775 if !held && !incoming_non_fungible_balance_set.contains(non_fungible) {
1776 return Err(ClientError::TransactionRequestError(
1777 TransactionRequestError::MissingNonFungibleAsset(non_fungible.faucet_id()),
1778 ));
1779 }
1780 }
1781
1782 Ok(())
1783}
1784
1785pub(crate) async fn fetch_public_account_inputs(
1795 store: &Arc<dyn Store>,
1796 rpc_api: &Arc<dyn NodeRpcClient>,
1797 account_id: AccountId,
1798 storage_requirements: AccountStorageRequirements,
1799 account_state_at: AccountStateAt,
1800) -> Result<AccountInputs, ClientError> {
1801 let known_code: Option<AccountCode> =
1802 store.get_foreign_account_code(vec![account_id]).await?.into_values().next();
1803
1804 let vault = store
1807 .get_account_header(account_id)
1808 .await?
1809 .map_or(VaultFetch::Always, |(header, ..)| {
1810 VaultFetch::IfChangedFrom(header.vault_root())
1811 });
1812
1813 let (_block_num, account_proof) = rpc_api
1814 .get_account(
1815 account_id,
1816 GetAccountRequest::new()
1817 .with_storage(StorageMapFetch::Slots(storage_requirements.clone()))
1818 .at(account_state_at)
1819 .with_known_code(known_code)
1820 .with_vault(vault),
1821 )
1822 .await?;
1823
1824 let account_inputs = request::account_proof_into_inputs(account_proof)?;
1825
1826 let _ = store
1827 .upsert_foreign_account_code(account_id, account_inputs.code().clone())
1828 .await
1829 .inspect_err(|err| {
1830 tracing::warn!(
1831 %account_id,
1832 %err,
1833 "Failed to persist foreign account code to store"
1834 );
1835 });
1836
1837 Ok(account_inputs)
1838}
1839
1840fn promote_indeterminate_submission(
1843 err: RpcError,
1844 transaction: ProvenTransaction,
1845 transaction_inputs: TransactionInputs,
1846) -> ClientError {
1847 if !err.is_indeterminate_submission() {
1848 return ClientError::RpcError(err);
1849 }
1850
1851 ClientError::SubmissionOutcomeUnknown {
1852 transaction: Box::new(transaction),
1853 transaction_inputs: Box::new(transaction_inputs),
1854 source: err,
1855 }
1856}
1857
1858pub fn notes_from_output(output_notes: &RawOutputNotes) -> impl Iterator<Item = &Note> {
1863 output_notes.iter().filter_map(|n| match n {
1864 RawOutputNote::Full(n) => Some(n),
1865 RawOutputNote::Partial(_) => None,
1866 })
1867}
1868
1869pub(crate) fn validate_executed_transaction(
1872 executed_transaction: &ExecutedTransaction,
1873 expected_output_recipients: &[NoteRecipient],
1874) -> Result<(), ClientError> {
1875 let tx_output_recipient_digests = executed_transaction
1876 .output_notes()
1877 .iter()
1878 .filter_map(|n| n.recipient().map(NoteRecipient::digest))
1879 .collect::<Vec<_>>();
1880
1881 let missing_recipient_digest: Vec<Word> = expected_output_recipients
1882 .iter()
1883 .filter_map(|recipient| {
1884 (!tx_output_recipient_digests.contains(&recipient.digest()))
1885 .then_some(recipient.digest())
1886 })
1887 .collect();
1888
1889 if !missing_recipient_digest.is_empty() {
1890 return Err(ClientError::MissingOutputRecipients(missing_recipient_digest));
1891 }
1892
1893 Ok(())
1894}
1895
1896#[cfg(test)]
1900mod tests {
1901 use alloc::vec;
1902
1903 use miden_protocol::Word;
1904 use miden_protocol::account::auth::AuthSecretKey;
1905 use miden_protocol::account::{
1906 Account,
1907 AccountBuilder,
1908 AccountComponent,
1909 AccountComponentMetadata,
1910 AccountId,
1911 AccountType,
1912 };
1913 use miden_protocol::asset::FungibleAsset;
1914 use miden_protocol::block::{BlockHeader, BlockNumber, FeeParameters};
1915 use miden_protocol::crypto::rand::RandomCoin;
1916 use miden_protocol::note::{Note, NoteType};
1917 use miden_protocol::testing::account_id::{
1918 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
1919 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
1920 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
1921 ACCOUNT_ID_SENDER,
1922 };
1923 use miden_protocol::testing::validator_keys::random_validator_set;
1924 use miden_standards::account::AccountBuilderSchemaCommitmentExt;
1925 use miden_standards::account::auth::{
1926 Approver,
1927 ApproverSet,
1928 AuthGuardedMultisig,
1929 AuthGuardedMultisigConfig,
1930 AuthMultisig,
1931 AuthMultisigConfig,
1932 AuthMultisigSmart,
1933 AuthMultisigSmartConfig,
1934 AuthSingleSig,
1935 FeeConversionInfo,
1936 GuardianConfig,
1937 NoAuth,
1938 commit_fee_conversion_info,
1939 };
1940 use miden_standards::account::wallets::BasicWallet;
1941 use miden_standards::note::P2idNote;
1942
1943 use super::{
1944 AccountComponentInterface,
1945 NATIVE_FEE_CONVERSION_SALT,
1946 TransactionRequest,
1947 TransactionRequestBuilder,
1948 attach_native_fee_conversion_info,
1949 validate_fee_conversion_info_support,
1950 validate_output_note_senders,
1951 };
1952 use crate::ClientError;
1953 use crate::assembly::CodeBuilder;
1954 use crate::auth::AuthSchemeId;
1955 use crate::transaction::TransactionRequestError;
1956
1957 fn own_note_with_sender(sender: AccountId) -> Note {
1958 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
1959 let target_id =
1960 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1961 let mut rng = RandomCoin::new(Word::default());
1962
1963 P2idNote::builder()
1964 .sender(sender)
1965 .target(target_id)
1966 .asset(FungibleAsset::new(faucet_id, 100).unwrap())
1967 .note_type(NoteType::Public)
1968 .generate_serial_number(&mut rng)
1969 .build()
1970 .expect("note creation failed")
1971 .into()
1972 }
1973
1974 #[test]
1975 fn output_note_with_foreign_sender_is_rejected() {
1976 let account_id =
1977 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1978 let foreign_sender = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
1979 assert_ne!(account_id, foreign_sender);
1980
1981 let request = TransactionRequestBuilder::new()
1982 .own_output_notes(vec![own_note_with_sender(foreign_sender)])
1983 .build()
1984 .unwrap();
1985
1986 let err = validate_output_note_senders(&request, account_id).unwrap_err();
1987 match err {
1988 ClientError::TransactionRequestError(
1989 TransactionRequestError::OutputNoteSenderMismatch { expected, actual },
1990 ) => {
1991 assert_eq!(expected, account_id);
1992 assert_eq!(actual, foreign_sender);
1993 },
1994 other => panic!("expected OutputNoteSenderMismatch, got {other:?}"),
1995 }
1996 }
1997
1998 #[test]
1999 fn output_note_with_matching_sender_is_accepted() {
2000 let account_id =
2001 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
2002
2003 let request = TransactionRequestBuilder::new()
2004 .own_output_notes(vec![own_note_with_sender(account_id)])
2005 .build()
2006 .unwrap();
2007
2008 validate_output_note_senders(&request, account_id).unwrap();
2009 }
2010
2011 #[test]
2012 fn request_without_own_output_notes_is_accepted() {
2013 let account_id =
2014 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
2015 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
2016
2017 let request = TransactionRequestBuilder::new()
2019 .input_notes(vec![(own_note_with_sender(faucet_id), None)])
2020 .build()
2021 .unwrap();
2022
2023 validate_output_note_senders(&request, account_id).unwrap();
2024 }
2025
2026 fn account_with_auth(auth_component: impl Into<AccountComponent>) -> Account {
2028 AccountBuilder::new([7u8; 32])
2029 .account_type(AccountType::Public)
2030 .with_component(auth_component)
2031 .with_component(BasicWallet)
2032 .build_with_schema_commitment()
2033 .expect("account creation failed")
2034 }
2035
2036 fn fee_conversion_request() -> TransactionRequest {
2037 TransactionRequestBuilder::new()
2038 .fee_conversion_salt(Word::from([13u32, 14, 15, 16]))
2039 .build()
2040 .unwrap()
2041 }
2042
2043 #[test]
2044 fn fee_conversion_info_is_accepted_by_a_signature_authenticated_account() {
2045 let key = AuthSecretKey::new_falcon512_poseidon2();
2046 let auth = AuthSingleSig::new(Approver::new(
2047 key.public_key().to_commitment(),
2048 AuthSchemeId::Falcon512Poseidon2,
2049 ));
2050
2051 validate_fee_conversion_info_support(
2052 &fee_conversion_request(),
2053 &account_with_auth(auth).code_interface(),
2054 )
2055 .unwrap();
2056 }
2057
2058 #[test]
2059 fn fee_conversion_info_is_rejected_by_an_account_that_cannot_read_it() {
2060 let account = account_with_auth(NoAuth);
2061
2062 let err = validate_fee_conversion_info_support(
2063 &fee_conversion_request(),
2064 &account.code_interface(),
2065 )
2066 .expect_err("NoAuth does not read the auth args");
2067 match err {
2068 ClientError::TransactionRequestError(
2069 TransactionRequestError::FeeConversionInfoUnsupported(auth_component),
2070 ) => assert_eq!(auth_component, AccountComponentInterface::AuthNoAuth.name()),
2071 other => panic!("expected FeeConversionInfoUnsupported, got {other:?}"),
2072 }
2073 }
2074
2075 #[test]
2076 fn a_request_without_fee_conversion_info_skips_the_auth_component_check() {
2077 validate_fee_conversion_info_support(
2079 &TransactionRequestBuilder::new().build().unwrap(),
2080 &account_with_auth(NoAuth).code_interface(),
2081 )
2082 .unwrap();
2083 }
2084
2085 const NATIVE_FEE_FAUCET: u128 = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET;
2091
2092 fn header_with_base_fee(verification_base_fee: u32) -> BlockHeader {
2095 let fee_parameters = FeeParameters::new(
2096 AccountId::try_from(NATIVE_FEE_FAUCET).unwrap(),
2097 verification_base_fee,
2098 );
2099 let (_, validator_keys) = random_validator_set(1);
2100
2101 BlockHeader::new(
2102 1,
2103 Word::empty(),
2104 BlockNumber::from(1u32),
2105 Word::empty(),
2106 Word::empty(),
2107 Word::empty(),
2108 Word::empty(),
2109 Word::empty(),
2110 Word::empty(),
2111 validator_keys,
2112 fee_parameters,
2113 0,
2114 )
2115 }
2116
2117 fn injected_auth_arg(
2120 mut request: TransactionRequest,
2121 account: &Account,
2122 verification_base_fee: u32,
2123 ) -> Option<Word> {
2124 let _ = attach_native_fee_conversion_info(
2125 &mut request,
2126 &account.code_interface(),
2127 &header_with_base_fee(verification_base_fee),
2128 );
2129 *request.auth_arg()
2130 }
2131
2132 fn try_injected_auth_arg(
2134 mut request: TransactionRequest,
2135 account: &Account,
2136 verification_base_fee: u32,
2137 ) -> Result<Option<Word>, ClientError> {
2138 attach_native_fee_conversion_info(
2139 &mut request,
2140 &account.code_interface(),
2141 &header_with_base_fee(verification_base_fee),
2142 )?;
2143 Ok(*request.auth_arg())
2144 }
2145
2146 fn singlesig_account() -> Account {
2147 let key = AuthSecretKey::new_falcon512_poseidon2();
2148 account_with_auth(AuthSingleSig::new(Approver::new(
2149 key.public_key().to_commitment(),
2150 AuthSchemeId::Falcon512Poseidon2,
2151 )))
2152 }
2153
2154 fn guarded_multisig_account() -> Account {
2155 let approvers = ApproverSet::new(
2156 vec![Approver::new(
2157 AuthSecretKey::new_falcon512_poseidon2().public_key().to_commitment(),
2158 AuthSchemeId::Falcon512Poseidon2,
2159 )],
2160 1,
2161 )
2162 .unwrap();
2163
2164 let guardian = GuardianConfig::new(Approver::new(
2165 AuthSecretKey::new_falcon512_poseidon2().public_key().to_commitment(),
2166 AuthSchemeId::Falcon512Poseidon2,
2167 ));
2168
2169 account_with_auth(
2170 AuthGuardedMultisig::new(AuthGuardedMultisigConfig::new(approvers, guardian).unwrap())
2171 .unwrap(),
2172 )
2173 }
2174
2175 #[test]
2176 fn native_fee_conversion_info_is_attached_on_a_fee_charging_chain() {
2177 let auth_arg = injected_auth_arg(
2178 TransactionRequestBuilder::new().build().unwrap(),
2179 &singlesig_account(),
2180 500,
2181 )
2182 .expect("a fee-charging chain should get conversion info attached");
2183
2184 let (expected, _) = commit_fee_conversion_info(
2185 FeeConversionInfo::one_to_one(AccountId::try_from(NATIVE_FEE_FAUCET).unwrap()),
2186 NATIVE_FEE_CONVERSION_SALT,
2187 );
2188 assert_eq!(auth_arg, expected, "the fee should be paid in the native asset at rate 1/1");
2189 }
2190
2191 #[test]
2192 fn an_explicit_auth_arg_is_not_overwritten() {
2193 let auth_arg = Word::from([21u32, 22, 23, 24]);
2194 let request = TransactionRequestBuilder::new().auth_arg(auth_arg).build().unwrap();
2195
2196 assert_eq!(
2197 injected_auth_arg(request, &singlesig_account(), 500),
2198 Some(auth_arg),
2199 "a request that declares its own auth arg keeps it"
2200 );
2201 }
2202
2203 fn native_commitment(salt: Word) -> Word {
2205 let (auth_arg, _) = commit_fee_conversion_info(
2206 FeeConversionInfo::one_to_one(AccountId::try_from(NATIVE_FEE_FAUCET).unwrap()),
2207 salt,
2208 );
2209 auth_arg
2210 }
2211
2212 #[test]
2213 fn a_declared_salt_is_used_for_the_native_commitment() {
2214 let salt = Word::from([17u32, 18, 19, 20]);
2215 let request = TransactionRequestBuilder::new().fee_conversion_salt(salt).build().unwrap();
2216
2217 let auth_arg = injected_auth_arg(request, &singlesig_account(), 500)
2218 .expect("a declared salt should still get native conversion info attached");
2219
2220 assert_eq!(auth_arg, native_commitment(salt));
2221 }
2222
2223 fn multisig_account() -> Account {
2224 let approvers = ApproverSet::new(
2225 vec![Approver::new(
2226 AuthSecretKey::new_falcon512_poseidon2().public_key().to_commitment(),
2227 AuthSchemeId::Falcon512Poseidon2,
2228 )],
2229 1,
2230 )
2231 .unwrap();
2232
2233 account_with_auth(AuthMultisig::new(AuthMultisigConfig::new(approvers)).unwrap())
2234 }
2235
2236 #[test]
2237 fn nothing_is_attached_where_it_is_not_needed_or_not_readable() {
2238 for (case, account, base_fee) in [
2239 ("a zero base fee charges nothing", singlesig_account(), 0),
2240 ("NoAuth never reads the auth args", account_with_auth(NoAuth), 500),
2241 ("a multisig salt is its own replay guard", multisig_account(), 500),
2242 ] {
2243 assert_eq!(
2244 injected_auth_arg(
2245 TransactionRequestBuilder::new().build().unwrap(),
2246 &account,
2247 base_fee
2248 ),
2249 None,
2250 "{case}"
2251 );
2252 }
2253 }
2254
2255 #[test]
2262 fn fee_conversion_info_is_accepted_by_a_guarded_multisig_account() {
2263 validate_fee_conversion_info_support(
2264 &fee_conversion_request(),
2265 &guarded_multisig_account().code_interface(),
2266 )
2267 .expect("a guarded multisig reads the auth args as conversion info");
2268 }
2269
2270 #[test]
2274 fn a_guarded_multisig_account_must_declare_its_own_fee_conversion_info() {
2275 let err = try_injected_auth_arg(
2276 TransactionRequestBuilder::new().build().unwrap(),
2277 &guarded_multisig_account(),
2278 500,
2279 )
2280 .expect_err("a guarded multisig account cannot inherit the fixed native salt");
2281 match err {
2282 ClientError::TransactionRequestError(
2283 TransactionRequestError::FeeConversionInfoRequired(auth_component),
2284 ) => {
2285 assert_eq!(auth_component, AccountComponentInterface::AuthGuardedMultisig.name());
2286 },
2287 other => panic!("expected FeeConversionInfoRequired, got {other:?}"),
2288 }
2289
2290 let salt = Word::from([13u32, 14, 15, 16]);
2291 assert_eq!(
2292 try_injected_auth_arg(fee_conversion_request(), &guarded_multisig_account(), 500)
2293 .expect("a declared salt is accepted"),
2294 Some(native_commitment(salt)),
2295 "a guarded multisig account that declares a salt commits the native conversion info"
2296 );
2297
2298 assert_eq!(
2299 try_injected_auth_arg(
2300 TransactionRequestBuilder::new().build().unwrap(),
2301 &guarded_multisig_account(),
2302 0
2303 )
2304 .expect("a chain charging nothing needs no conversion info"),
2305 None,
2306 );
2307 }
2308
2309 fn smart_multisig_account() -> Account {
2313 let approvers = ApproverSet::new(
2314 vec![Approver::new(
2315 AuthSecretKey::new_falcon512_poseidon2().public_key().to_commitment(),
2316 AuthSchemeId::Falcon512Poseidon2,
2317 )],
2318 1,
2319 )
2320 .unwrap();
2321
2322 account_with_auth(AuthMultisigSmart::new(AuthMultisigSmartConfig::new(approvers)).unwrap())
2323 }
2324
2325 #[test]
2330 fn fee_conversion_info_is_accepted_by_a_smart_multisig_account() {
2331 validate_fee_conversion_info_support(
2332 &fee_conversion_request(),
2333 &smart_multisig_account().code_interface(),
2334 )
2335 .expect("a smart multisig reads the auth args as conversion info");
2336 }
2337
2338 #[test]
2342 fn a_smart_multisig_account_must_declare_its_own_fee_conversion_info() {
2343 let err = try_injected_auth_arg(
2344 TransactionRequestBuilder::new().build().unwrap(),
2345 &smart_multisig_account(),
2346 500,
2347 )
2348 .expect_err("a smart multisig account cannot inherit the fixed native salt");
2349 match err {
2350 ClientError::TransactionRequestError(
2351 TransactionRequestError::FeeConversionInfoRequired(auth_component),
2352 ) => {
2353 assert_eq!(auth_component, AccountComponentInterface::AuthMultisigSmart.name());
2354 },
2355 other => panic!("expected FeeConversionInfoRequired, got {other:?}"),
2356 }
2357
2358 let salt = Word::from([13u32, 14, 15, 16]);
2359 assert_eq!(
2360 try_injected_auth_arg(fee_conversion_request(), &smart_multisig_account(), 500)
2361 .expect("a declared salt is accepted"),
2362 Some(native_commitment(salt)),
2363 "a smart multisig account that declares a salt commits the native conversion info"
2364 );
2365
2366 assert_eq!(
2367 try_injected_auth_arg(
2368 TransactionRequestBuilder::new().build().unwrap(),
2369 &smart_multisig_account(),
2370 0
2371 )
2372 .expect("a chain charging nothing needs no conversion info"),
2373 None,
2374 );
2375 }
2376
2377 #[test]
2380 fn an_unrecognized_auth_component_is_rejected_rather_than_panicking() {
2381 const CUSTOM_AUTH: &str = "
2382 use miden::protocol::native_account
2383
2384 @auth_script
2385 pub proc auth_custom
2386 exec.native_account::incr_nonce
2387 drop
2388 end
2389 ";
2390
2391 let code = CodeBuilder::default()
2392 .compile_component_code("miden::testing::custom_auth", CUSTOM_AUTH)
2393 .expect("custom auth component code should compile");
2394 let auth = AccountComponent::new(
2395 code,
2396 vec![],
2397 AccountComponentMetadata::new("miden::testing::custom_auth"),
2398 )
2399 .expect("custom auth component");
2400
2401 let account = account_with_auth(auth);
2402
2403 let err = validate_fee_conversion_info_support(
2404 &fee_conversion_request(),
2405 &account.code_interface(),
2406 )
2407 .expect_err("an account with no recognized auth component cannot read conversion info");
2408 assert!(matches!(
2409 err,
2410 ClientError::TransactionRequestError(
2411 TransactionRequestError::FeeConversionInfoUnsupported(_)
2412 )
2413 ));
2414
2415 assert_eq!(
2416 try_injected_auth_arg(TransactionRequestBuilder::new().build().unwrap(), &account, 500)
2417 .expect("a request declaring nothing is left alone"),
2418 None,
2419 "an auth component nothing can reason about gets nothing attached"
2420 );
2421 }
2422}