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;
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::protocol_config::ProtocolConfig;
85use miden_protocol::transaction::{AccountInputs, PartialBlockchain};
86use miden_protocol::vm::MIN_STACK_DEPTH;
87use miden_protocol::{Felt, Word};
88use miden_standards::account::auth::FeeConversionInfo;
89use miden_standards::account::faucets::FungibleFaucet;
90use miden_standards::account::interface::AccountComponentInterfaceExt;
91use miden_standards::note::TxFeeNote;
92use miden_tx::{DataStore, NoteConsumptionChecker, TransactionExecutor};
93use tracing::info;
94
95use super::Client;
96use crate::ClientError;
97use crate::note::{NoteScreenerError, NoteUpdateTracker, StandardNote};
98use crate::rpc::domain::account::{
99 AccountStorageRequirements,
100 GetAccountRequest,
101 StorageMapFetch,
102 VaultFetch,
103};
104use crate::rpc::encryption::{TransactionEncryptionKey, seal_transaction_inputs};
105use crate::rpc::{AccountStateAt, NodeRpcClient, RpcError};
106use crate::store::data_store::{ClientDataStore, build_partial_mmr_with_paths};
107use crate::store::input_note_states::ExpectedNoteState;
108use crate::store::{
109 AccountRecord,
110 InputNoteRecord,
111 InputNoteState,
112 NoteFilter,
113 NoteRecordError,
114 OutputNoteRecord,
115 Store,
116 StoreError,
117 TransactionFilter,
118};
119use crate::sync::NoteTagRecord;
120use crate::transaction::batch::InMemoryBatchDataStore;
121
122pub mod batch;
123pub use batch::{BatchBuilder, BatchBuilderError};
124
125mod chain_anchor;
126pub use chain_anchor::{ChainAnchor, ChainAnchorError};
127
128#[cfg(any())]
129mod dap_executor;
130mod prover;
131pub use prover::TransactionProver;
132
133mod record;
134pub use record::{
135 DiscardCause,
136 TransactionDetails,
137 TransactionRecord,
138 TransactionStatus,
139 TransactionStatusVariant,
140};
141
142mod store_update;
143pub use store_update::TransactionStoreUpdate;
144
145mod request;
146pub use request::{
147 ForeignAccount,
148 NoteArgs,
149 PaymentNoteDescription,
150 PswapTransactionData,
151 SwapTransactionData,
152 TransactionRequest,
153 TransactionRequestBuilder,
154 TransactionRequestError,
155 TransactionScriptTemplate,
156 build_fpi_script,
157};
158
159mod observer;
160pub use observer::TransactionObserver;
161
162mod result;
163pub use miden_protocol::transaction::{
166 ExecutedTransaction,
167 InputNote,
168 InputNotes,
169 OutputNote,
170 OutputNotes,
171 ProvenTransaction,
172 PublicOutputNote,
173 RawOutputNote,
174 RawOutputNotes,
175 TransactionArgs,
176 TransactionFee,
177 TransactionFeeError,
178 TransactionId,
179 TransactionInputs,
180 TransactionKernel,
181 TransactionScript,
182 TransactionScriptRoot,
183 TransactionSummary,
184};
185pub use miden_protocol::vm::{AdviceInputs, AdviceMap};
186pub use miden_standards::account::interface::{AccountComponentInterface, AccountInterface};
187pub use miden_standards::tx_script::{
188 ExpirationTransactionScript,
189 SendNotesTransactionScriptError,
190};
191pub use miden_tx::auth::TransactionAuthenticator;
192pub use miden_tx::{
193 DataStoreError,
194 LocalTransactionProver,
195 Prover,
196 TransactionExecutorError,
197 TransactionProverError,
198};
199pub use result::TransactionResult;
200
201pub(crate) const NATIVE_FEE_CONVERSION_SALT: Word = Word::empty();
208
209impl<AUTH> Client<AUTH>
211where
212 AUTH: TransactionAuthenticator + Sync + 'static,
213{
214 pub async fn get_transactions(
219 &self,
220 filter: TransactionFilter,
221 ) -> Result<Vec<TransactionRecord>, ClientError> {
222 self.store.get_transactions(filter).await.map_err(Into::into)
223 }
224
225 pub fn new_transaction_batch(&mut self) -> BatchBuilder<'_, AUTH> {
232 let inner_data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
233 BatchBuilder {
234 client: self,
235 data_store: InMemoryBatchDataStore::new(inner_data_store),
236 pushed_txs: Vec::new(),
237 consumed_input_notes: BTreeSet::new(),
238 }
239 }
240
241 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(
264 &mut self,
265 account_id: AccountId,
266 transaction_request: TransactionRequest,
267 tx_prover: Arc<dyn TransactionProver>,
268 ) -> Result<TransactionId, ClientError> {
269 if !transaction_request.expected_ntx_scripts().is_empty() {
272 Box::pin(self.ensure_ntx_scripts_registered(
273 account_id,
274 transaction_request.expected_ntx_scripts(),
275 tx_prover.clone(),
276 ))
277 .await?;
278 }
279
280 let tx_result = self.execute_transaction(account_id, transaction_request).await?;
281 let tx_id = tx_result.executed_transaction().id();
282
283 let proven_transaction = self.prove_transaction_with(&tx_result, tx_prover).await?;
284 let submission_height =
285 self.submit_proven_transaction(proven_transaction, &tx_result).await?;
286
287 let tx_update =
295 Box::new(self.get_transaction_store_update(&tx_result, submission_height).await?);
296
297 if let Err(apply_err) = self.apply_transaction_update((*tx_update).clone()).await {
298 info!(
299 "apply_transaction_update failed for submitted tx {tx_id}; returning \
300 ApplyTransactionAfterSubmitFailed with the pending update attached: {apply_err}"
301 );
302 return Err(ClientError::ApplyTransactionAfterSubmitFailed {
303 pending_update: tx_update,
304 source: Box::new(apply_err),
305 });
306 }
307
308 for observer in &self.transaction_observers {
312 crate::errors::log_observer_failure(
313 observer.name(),
314 "TransactionObserver::apply",
315 observer.apply(&tx_result).await,
316 );
317 }
318
319 Ok(tx_id)
320 }
321
322 pub async fn execute_transaction(
332 &self,
333 account_id: AccountId,
334 transaction_request: TransactionRequest,
335 ) -> Result<TransactionResult, ClientError> {
336 Box::pin(self.execute_transaction_with_mode(
337 account_id,
338 transaction_request,
339 TransactionExecutionMode::Standard,
340 None,
341 ))
342 .await
343 }
344
345 pub async fn execute_transaction_at(
378 &mut self,
379 account_id: AccountId,
380 transaction_request: TransactionRequest,
381 anchor: ChainAnchor,
382 ) -> Result<TransactionResult, ClientError> {
383 let result = self
384 .execute_transaction_with_mode(
385 account_id,
386 transaction_request,
387 TransactionExecutionMode::Standard,
388 Some(Box::new(anchor)),
389 )
390 .await?;
391
392 let expiration = result.executed_transaction().expiration_block_num();
397 let sync_height = self.store.get_sync_height().await?;
398 if expiration <= sync_height {
399 return Err(
400 ChainAnchorError::AnchoredTransactionExpired { expiration, sync_height }.into()
401 );
402 }
403
404 Ok(result)
405 }
406
407 async fn chain_anchor_at_tip(
412 &self,
413 tracked_blocks: BTreeSet<BlockNumber>,
414 ) -> Result<ChainAnchor, ClientError> {
415 let sync_height = self.store.get_sync_height().await?;
416
417 let (header, _had_notes) = self
418 .store
419 .get_block_header_by_num(sync_height)
420 .await?
421 .ok_or(StoreError::BlockHeaderNotFound(sync_height))?;
422
423 let mut tracked_blocks = tracked_blocks;
424 tracked_blocks.remove(&sync_height);
426
427 let block_headers: Vec<BlockHeader> = self
428 .store
429 .get_block_headers(&tracked_blocks)
430 .await?
431 .into_iter()
432 .map(|(header, _has_notes)| header)
433 .collect();
434
435 let fetched_nums: BTreeSet<BlockNumber> =
438 block_headers.iter().map(BlockHeader::block_num).collect();
439 if let Some(&missing) = tracked_blocks.difference(&fetched_nums).next() {
440 return Err(StoreError::BlockHeaderNotFound(missing).into());
441 }
442
443 let peaks = self.store.get_current_blockchain_peaks().await?;
444 let partial_mmr = build_partial_mmr_with_paths(&self.store, peaks, &block_headers).await?;
445
446 let chain = PartialBlockchain::new(partial_mmr, block_headers)?;
447
448 Ok(ChainAnchor::new(header, chain)?)
449 }
450
451 pub async fn chain_anchor_for_request(
470 &self,
471 transaction_request: &TransactionRequest,
472 ) -> Result<ChainAnchor, ClientError> {
473 let inferred_input_note_ids: Vec<NoteId> = transaction_request
474 .input_note_ids()
475 .filter(|note_id| !transaction_request.explicit_input_notes.contains_key(note_id))
476 .collect();
477
478 let mut tracked_blocks: BTreeSet<BlockNumber> = if inferred_input_note_ids.is_empty() {
479 BTreeSet::new()
480 } else {
481 self.store
482 .get_input_notes(NoteFilter::List(inferred_input_note_ids))
483 .await?
484 .iter()
485 .filter(|record| record.is_authenticated())
486 .filter_map(|record| record.inclusion_proof())
487 .map(|proof| proof.location().block_num())
488 .collect()
489 };
490 tracked_blocks.extend(
491 transaction_request
492 .explicit_input_notes
493 .values()
494 .filter_map(InputNote::proof)
495 .map(|proof| proof.location().block_num()),
496 );
497
498 self.chain_anchor_at_tip(tracked_blocks).await
499 }
500
501 #[cfg(any())]
515 pub async fn execute_transaction_with_dap(
516 &self,
517 account_id: AccountId,
518 transaction_request: TransactionRequest,
519 ) -> Result<TransactionResult, ClientError> {
520 self.execute_transaction_with_mode(
521 account_id,
522 transaction_request,
523 TransactionExecutionMode::Dap,
524 None,
525 )
526 .await
527 }
528
529 async fn execute_transaction_with_mode(
533 &self,
534 account_id: AccountId,
535 transaction_request: TransactionRequest,
536 execution_mode: TransactionExecutionMode,
537 anchor: Option<Box<ChainAnchor>>,
538 ) -> Result<TransactionResult, ClientError> {
539 let account: PartialAccount =
540 self.get_native_account_record(account_id).await?.try_into()?;
541
542 let prep = self
543 .prepare_transaction(&account, transaction_request, anchor.as_deref())
544 .await?;
545
546 let mut data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
547 if let Some(anchor) = anchor {
548 data_store = data_store.with_chain_anchor(*anchor);
549 }
550 data_store.register_note_scripts(prep.output_note_scripts());
551 for fpi_account in &prep.foreign_account_inputs {
552 data_store.mast_store().load_account_code(fpi_account.code());
553 }
554 data_store.register_foreign_account_inputs(prep.foreign_account_inputs);
555
556 data_store.mast_store().load_account_code(account.code());
557
558 let mut notes = prep.notes;
559 if prep.ignore_invalid_notes {
560 notes = self
561 .get_valid_input_notes(
562 &data_store,
563 account.id(),
564 prep.block_num,
565 notes,
566 prep.tx_args.clone(),
567 )
568 .await?;
569 }
570
571 let executed_transaction = match execution_mode {
572 TransactionExecutionMode::Standard => {
573 self.build_executor(&data_store)?
574 .execute_transaction(account_id, prep.block_num, notes, prep.tx_args)
575 .await?
576 },
577 #[cfg(any())]
578 TransactionExecutionMode::Dap => {
579 self.build_dap_executor(&data_store)?
580 .execute_transaction(account_id, prep.block_num, notes, prep.tx_args)
581 .await?
582 },
583 };
584
585 validate_executed_transaction(&executed_transaction, &prep.output_recipients)?;
586 TransactionResult::new(executed_transaction, prep.future_notes)
587 }
588
589 pub(crate) async fn prepare_transaction(
605 &self,
606 account: &PartialAccount,
607 transaction_request: TransactionRequest,
608 anchor: Option<&ChainAnchor>,
609 ) -> Result<PreparedTransaction, ClientError> {
610 self.validate_account_request(
611 &transaction_request,
612 account.id(),
613 &account.code_interface(),
614 )
615 .await?;
616
617 self.prepare_transaction_inner(account.code_interface(), transaction_request, anchor)
618 .await
619 }
620
621 pub(crate) async fn prepare_transaction_for_batch(
622 &self,
623 account: &PartialAccount,
624 transaction_request: TransactionRequest,
625 ) -> Result<PreparedTransaction, ClientError> {
626 self.prepare_transaction_inner(account.code_interface(), transaction_request, None)
627 .await
628 }
629
630 async fn prepare_transaction_inner(
631 &self,
632 account_code_interface: AccountCodeInterface,
633 mut transaction_request: TransactionRequest,
634 anchor: Option<&ChainAnchor>,
635 ) -> Result<PreparedTransaction, ClientError> {
636 if anchor.is_none() {
637 self.validate_recency().await?;
638 }
639
640 let mut stored_note_records = self
642 .store
643 .get_input_notes(NoteFilter::List(transaction_request.input_note_ids().collect()))
644 .await?;
645
646 for note in &stored_note_records {
648 if note.is_consumed() {
649 return Err(ClientError::TransactionRequestError(
650 TransactionRequestError::InputNoteAlreadyConsumed(note.details_commitment()),
651 ));
652 }
653 }
654
655 stored_note_records.retain(InputNoteRecord::is_authenticated);
657
658 let notes = transaction_request.build_input_notes(stored_note_records)?;
659
660 if let Some(anchor) = anchor {
664 for note in notes.iter() {
665 if let Some(location) = note.location() {
666 let block_num = location.block_num();
667 if block_num < anchor.block_num()
668 && !anchor.partial_blockchain().contains_block(block_num)
669 {
670 return Err(ChainAnchorError::BlockNotTracked { block_num }.into());
671 }
672 }
673 }
674 }
675
676 let output_recipients =
677 transaction_request.expected_output_recipients().cloned().collect::<Vec<_>>();
678
679 let future_notes: Vec<(NoteDetails, NoteTag)> =
680 transaction_request.expected_future_notes().cloned().collect();
681
682 let tx_script = transaction_request.build_transaction_script(&account_code_interface)?;
683
684 let foreign_accounts = transaction_request.foreign_accounts().clone();
685
686 let block_num = match anchor {
689 Some(anchor) => anchor.block_num(),
690 None => self.store.get_sync_height().await?,
691 };
692
693 let foreign_account_inputs =
694 self.retrieve_foreign_account_inputs(foreign_accounts, block_num).await?;
695
696 let ignore_invalid_notes = transaction_request.ignore_invalid_input_notes();
697
698 let reference_header = match anchor {
699 Some(anchor) => anchor.header().clone(),
700 None => {
701 self.store
702 .get_block_header_by_num(block_num)
703 .await?
704 .ok_or(StoreError::BlockHeaderNotFound(block_num))?
705 .0
706 },
707 };
708 attach_native_fee_conversion_info(
709 &mut transaction_request,
710 &account_code_interface,
711 &reference_header,
712 &self.get_protocol_config(reference_header.protocol_config_commitment()).await?,
713 )?;
714
715 let tx_args = transaction_request.into_transaction_args(tx_script);
716
717 Ok(PreparedTransaction {
718 notes,
719 output_recipients,
720 future_notes,
721 tx_args,
722 foreign_account_inputs,
723 block_num,
724 ignore_invalid_notes,
725 })
726 }
727
728 pub async fn prove_transaction(
730 &self,
731 tx_result: &TransactionResult,
732 ) -> Result<ProvenTransaction, ClientError> {
733 self.prove_transaction_with(tx_result, self.tx_prover.clone()).await
734 }
735
736 pub async fn prove_transaction_with(
744 &self,
745 tx_result: &TransactionResult,
746 tx_prover: Arc<dyn TransactionProver>,
747 ) -> Result<ProvenTransaction, ClientError> {
748 info!("Proving transaction...");
749
750 let executed_transaction = tx_result.executed_transaction();
751 let proven_transaction = tx_prover.prove(executed_transaction.clone().into()).await?;
752
753 if proven_transaction.id() != executed_transaction.id() {
762 return Err(ClientError::MismatchedProvenTransaction {
763 requested: executed_transaction.id(),
764 returned: proven_transaction.id(),
765 });
766 }
767
768 info!("Transaction proven.");
769
770 Ok(proven_transaction)
771 }
772
773 pub async fn submit_proven_transaction(
783 &mut self,
784 proven_transaction: ProvenTransaction,
785 transaction_inputs: impl Into<TransactionInputs>,
786 ) -> Result<BlockNumber, ClientError> {
787 info!("Submitting transaction to the network...");
788 let tx_id = proven_transaction.id();
789 let key = self.transaction_encryption_key().await?;
790
791 let transaction_inputs = transaction_inputs.into();
795 let submitted = proven_transaction.clone();
796
797 let sealed_inputs =
798 seal_transaction_inputs(&mut self.rng, &key, tx_id, &transaction_inputs)?;
799
800 let result =
801 self.rpc_api.submit_proven_transaction(proven_transaction, sealed_inputs).await;
802 if let Err(err) = &result {
803 self.forget_stale_transaction_encryption_key(err).await;
804 }
805
806 let block_num = result
807 .map_err(|err| promote_indeterminate_submission(err, submitted, transaction_inputs))?;
808 info!("Transaction submitted.");
809
810 Ok(block_num)
811 }
812
813 pub(crate) async fn transaction_encryption_key(
821 &self,
822 ) -> Result<TransactionEncryptionKey, ClientError> {
823 if let Some(key) = self.store.get_transaction_encryption_key().await? {
824 return Ok(key);
825 }
826
827 let attested = self.rpc_api.get_transaction_encryption_key().await?;
828
829 let genesis_commitment =
833 self.trusted_block_header(BlockNumber::GENESIS).await?.commitment();
834 let chain_tip = self.store.get_sync_height().await?;
835 let validator_keys = self.trusted_block_header(chain_tip).await?.validator_config().clone();
836
837 let key = attested.verify(genesis_commitment, &validator_keys)?;
838 self.store.set_transaction_encryption_key(&key).await?;
839
840 Ok(key)
841 }
842
843 #[cfg(feature = "testing")]
846 pub async fn seed_transaction_encryption_key(
847 &self,
848 key: TransactionEncryptionKey,
849 ) -> Result<(), ClientError> {
850 Ok(self.store.set_transaction_encryption_key(&key).await?)
851 }
852
853 pub(crate) async fn forget_stale_transaction_encryption_key(&self, err: &RpcError) {
859 if err.is_stale_transaction_encryption_key()
860 && let Err(err) = self.store.remove_transaction_encryption_key().await
861 {
862 tracing::warn!("failed to evict the stale transaction encryption key: {err}");
863 }
864 }
865
866 async fn trusted_block_header(
873 &self,
874 block_num: BlockNumber,
875 ) -> Result<BlockHeader, ClientError> {
876 self.store.get_block_header_by_num(block_num).await?.map(|(header, _)| header).ok_or_else(
877 || {
878 ClientError::ChainValidationError(alloc::format!(
879 "block header {block_num} is not tracked locally; sync the client before it can verify data against the chain"
880 ))
881 },
882 )
883 }
884
885 pub async fn get_transaction_store_update(
888 &self,
889 tx_result: &TransactionResult,
890 submission_height: BlockNumber,
891 ) -> Result<TransactionStoreUpdate, TransactionStoreUpdateError> {
892 let note_updates = self.get_note_updates(submission_height, tx_result).await?;
893
894 let new_tags: Vec<NoteTagRecord> = note_updates
897 .updated_input_notes()
898 .filter_map(|note| {
899 let note = note.inner();
900
901 if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) =
902 note.state()
903 {
904 Some(NoteTagRecord::with_note_source(*tag, note.details_commitment()))
905 } else {
906 None
907 }
908 })
909 .collect();
910
911 Ok(TransactionStoreUpdate::new(
912 tx_result.executed_transaction().clone(),
913 submission_height,
914 note_updates,
915 tx_result.future_notes().to_vec(),
916 new_tags,
917 ))
918 }
919
920 pub async fn apply_transaction(
923 &self,
924 tx_result: &TransactionResult,
925 submission_height: BlockNumber,
926 ) -> Result<(), ClientError> {
927 let tx_update = self.get_transaction_store_update(tx_result, submission_height).await?;
928
929 self.apply_transaction_update(tx_update).await?;
930
931 for observer in &self.transaction_observers {
933 if let Err(err) = observer.apply(tx_result).await {
934 tracing::warn!(
935 observer = observer.name(),
936 error = ?err,
937 "TransactionObserver::apply failed; continuing with remaining observers",
938 );
939 }
940 }
941
942 Ok(())
943 }
944
945 pub async fn apply_transaction_update(
946 &self,
947 tx_update: TransactionStoreUpdate,
948 ) -> Result<(), ClientError> {
949 info!("Applying transaction to the local store...");
952
953 let executed_transaction = tx_update.executed_transaction();
954 let account_id = executed_transaction.account_id();
955
956 if self.account_reader(account_id).status().await?.is_locked() {
957 return Err(ClientError::AccountLocked(account_id));
958 }
959
960 self.store.apply_transaction(tx_update).await?;
961 info!("Transaction stored.");
962 Ok(())
963 }
964
965 pub async fn execute_program(
970 &self,
971 account_id: AccountId,
972 tx_script: TransactionScript,
973 advice_inputs: AdviceInputs,
974 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
975 ) -> Result<[Felt; MIN_STACK_DEPTH], ClientError> {
976 let (data_store, block_ref) =
977 self.prepare_program_execution(account_id, foreign_accounts).await?;
978
979 Ok(self
980 .build_executor(&data_store)?
981 .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
982 .await?)
983 }
984
985 #[cfg(any())]
988 pub async fn execute_program_with_dap(
989 &self,
990 account_id: AccountId,
991 tx_script: TransactionScript,
992 advice_inputs: AdviceInputs,
993 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
994 ) -> Result<[Felt; MIN_STACK_DEPTH], ClientError> {
995 let (data_store, block_ref) =
996 self.prepare_program_execution(account_id, foreign_accounts).await?;
997
998 Ok(self
999 .build_dap_executor(&data_store)?
1000 .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
1001 .await?)
1002 }
1003
1004 pub async fn validate_request(
1014 &self,
1015 account_id: AccountId,
1016 transaction_request: &TransactionRequest,
1017 ) -> Result<(), ClientError> {
1018 self.validate_recency().await?;
1019 validate_output_note_senders(transaction_request, account_id)?;
1020 let account: PartialAccount = self
1021 .store
1022 .get_minimal_partial_account(account_id)
1023 .await?
1024 .ok_or(ClientError::AccountDataNotFound(account_id))?
1025 .try_into()?;
1026 self.validate_account_request(transaction_request, account_id, &account.code_interface())
1027 .await
1028 }
1029
1030 async fn validate_account_request(
1035 &self,
1036 transaction_request: &TransactionRequest,
1037 account_id: AccountId,
1038 account_code_interface: &AccountCodeInterface,
1039 ) -> Result<(), ClientError> {
1040 validate_fee_conversion_info_support(transaction_request, account_code_interface)?;
1041
1042 if account_code_interface.contains([FungibleFaucet::mint_and_send_root()]) {
1043 Ok(())
1045 } else {
1046 let assets = self.account_reader(account_id).assets().await?;
1047 validate_basic_account_request(transaction_request, &assets)
1048 }
1049 }
1050
1051 async fn validate_recency(&self) -> Result<(), ClientError> {
1052 if let Some(max_block_number_delta) = self.max_block_number_delta {
1053 let current_chain_tip =
1054 self.rpc_api.get_block_header_by_number(None, false).await?.0.block_num();
1055
1056 if current_chain_tip > self.store.get_sync_height().await? + max_block_number_delta {
1057 return Err(ClientError::RecencyConditionError(
1058 "The client is too far behind the chain tip to execute the transaction",
1059 ));
1060 }
1061 }
1062 Ok(())
1063 }
1064
1065 pub async fn ensure_ntx_scripts_registered(
1078 &mut self,
1079 account_id: AccountId,
1080 scripts: &[NoteScript],
1081 tx_prover: Arc<dyn TransactionProver>,
1082 ) -> Result<(), ClientError> {
1083 let mut missing_scripts = Vec::new();
1084
1085 for script in scripts {
1086 if StandardNote::from_script(script).is_some() {
1088 continue;
1089 }
1090
1091 let script_root = script.root();
1092
1093 match self.rpc_api.get_note_script_by_root(script_root.into()).await {
1095 Ok(Some(_)) => {},
1096 Ok(None) => missing_scripts.push(script.clone()),
1097 Err(source) => {
1098 return Err(ClientError::NtxScriptRegistrationFailed {
1099 script_root: script_root.into(),
1100 source,
1101 });
1102 },
1103 }
1104 }
1105
1106 if missing_scripts.is_empty() {
1107 return Ok(());
1108 }
1109
1110 let registration_request = TransactionRequestBuilder::new().build_register_note_scripts(
1111 account_id,
1112 missing_scripts,
1113 self.rng(),
1114 )?;
1115
1116 let tx_result = self.execute_transaction(account_id, registration_request).await?;
1117 let proven = self.prove_transaction_with(&tx_result, tx_prover).await?;
1118 let submission_height = self.submit_proven_transaction(proven, &tx_result).await?;
1119 self.apply_transaction(&tx_result, submission_height).await?;
1120
1121 Ok(())
1122 }
1123
1124 pub(crate) async fn get_valid_input_notes<STORE: DataStore + Sync>(
1133 &self,
1134 data_store: &STORE,
1135 account_id: AccountId,
1136 block_ref: BlockNumber,
1137 mut input_notes: InputNotes<InputNote>,
1138 tx_args: TransactionArgs,
1139 ) -> Result<InputNotes<InputNote>, ClientError> {
1140 loop {
1141 if input_notes.is_empty() {
1144 break;
1145 }
1146
1147 let execution = NoteConsumptionChecker::new(&self.build_executor(data_store)?)
1148 .check_notes_consumability(
1149 account_id,
1150 block_ref,
1151 input_notes.iter().map(|n| n.clone().into_note()).collect(),
1152 tx_args.clone(),
1153 )
1154 .await?;
1155
1156 if execution.failed().is_empty() {
1157 break;
1158 }
1159
1160 let failed_note_ids: BTreeSet<NoteId> =
1161 execution.failed().iter().map(|n| n.note().id()).collect();
1162 let filtered_input_notes = InputNotes::new(
1163 input_notes
1164 .into_iter()
1165 .filter(|note| !failed_note_ids.contains(¬e.id()))
1166 .collect(),
1167 )
1168 .expect("Created from a valid input notes list");
1169
1170 input_notes = filtered_input_notes;
1171 }
1172
1173 Ok(input_notes)
1174 }
1175
1176 async fn retrieve_foreign_account_inputs(
1185 &self,
1186 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
1187 block_num: BlockNumber,
1188 ) -> Result<Vec<AccountInputs>, ClientError> {
1189 if foreign_accounts.is_empty() {
1190 return Ok(Vec::new());
1191 }
1192
1193 let mut return_foreign_account_inputs = Vec::with_capacity(foreign_accounts.len());
1194
1195 for foreign_account in foreign_accounts.into_values() {
1196 let foreign_account_inputs = match foreign_account {
1197 ForeignAccount::Public(account_id, storage_requirements) => {
1198 fetch_public_account_inputs(
1199 &self.store,
1200 &self.rpc_api,
1201 account_id,
1202 storage_requirements,
1203 AccountStateAt::Block(block_num),
1204 )
1205 .await?
1206 },
1207 ForeignAccount::Private(partial_account) => {
1208 let account_id = partial_account.id();
1209 let (_, account_proof) = self
1210 .rpc_api
1211 .get_account(
1212 account_id,
1213 GetAccountRequest::new().at(AccountStateAt::Block(block_num)),
1214 )
1215 .await?;
1216 let (witness, _) = account_proof.into_parts();
1217 AccountInputs::new(partial_account, witness)
1218 },
1219 };
1220
1221 return_foreign_account_inputs.push(foreign_account_inputs);
1222 }
1223
1224 Ok(return_foreign_account_inputs)
1225 }
1226
1227 async fn prepare_program_execution(
1231 &self,
1232 account_id: AccountId,
1233 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
1234 ) -> Result<(ClientDataStore, BlockNumber), ClientError> {
1235 let block_ref = self.get_sync_height().await?;
1236
1237 let foreign_account_inputs =
1238 self.retrieve_foreign_account_inputs(foreign_accounts, block_ref).await?;
1239
1240 let account_code = self
1241 .store
1242 .get_account_code(account_id)
1243 .await?
1244 .ok_or(ClientError::AccountDataNotFound(account_id))?;
1245
1246 let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
1247
1248 data_store.mast_store().load_account_code(&account_code);
1250
1251 for fpi_account in &foreign_account_inputs {
1252 data_store.mast_store().load_account_code(fpi_account.code());
1253 }
1254
1255 data_store.register_foreign_account_inputs(foreign_account_inputs);
1256
1257 Ok((data_store, block_ref))
1258 }
1259
1260 pub(crate) fn build_executor<'store, 'auth, STORE: DataStore + Sync>(
1263 &'auth self,
1264 data_store: &'store STORE,
1265 ) -> Result<TransactionExecutor<'store, 'auth, STORE, AUTH>, TransactionExecutorError> {
1266 let mut executor = TransactionExecutor::new(data_store)
1267 .with_options(self.exec_options)?
1268 .with_source_manager(self.source_manager.clone());
1269 if let Some(authenticator) = self.authenticator.as_deref() {
1270 executor = executor.with_authenticator(authenticator);
1271 }
1272 Ok(executor)
1273 }
1274
1275 async fn get_native_account_record(
1280 &self,
1281 account_id: AccountId,
1282 ) -> Result<AccountRecord, ClientError> {
1283 let account_record = self
1284 .store
1285 .get_minimal_partial_account(account_id)
1286 .await?
1287 .ok_or(ClientError::AccountDataNotFound(account_id))?;
1288 if account_record.is_watched() {
1289 return Err(ClientError::AccountIsWatched(account_id));
1290 }
1291 Ok(account_record)
1292 }
1293
1294 #[cfg(any())]
1296 pub(crate) fn build_dap_executor<'store, 'auth, STORE: DataStore + Sync>(
1297 &'auth self,
1298 data_store: &'store STORE,
1299 ) -> Result<
1300 TransactionExecutor<'store, 'auth, STORE, AUTH, dap_executor::DapProgramExecutor>,
1301 TransactionExecutorError,
1302 > {
1303 Ok(self
1304 .build_executor(data_store)?
1305 .with_program_executor::<dap_executor::DapProgramExecutor>())
1306 }
1307
1308 async fn get_note_updates(
1311 &self,
1312 submission_height: BlockNumber,
1313 tx_result: &TransactionResult,
1314 ) -> Result<NoteUpdateTracker, TransactionStoreUpdateError> {
1315 let executed_tx = tx_result.executed_transaction();
1316 let current_timestamp = self.store.get_current_timestamp();
1317 let current_block_num = self.store.get_sync_height().await?;
1318
1319 let new_output_notes = executed_tx
1331 .output_notes()
1332 .iter()
1333 .filter(|output_note| {
1334 output_note
1335 .recipient()
1336 .is_none_or(|recipient| recipient.script().root() != TxFeeNote::script_root())
1337 })
1338 .cloned()
1339 .filter_map(|output_note| {
1340 OutputNoteRecord::try_from_output_note(output_note, submission_height).ok()
1341 })
1342 .collect::<Vec<_>>();
1343
1344 let mut new_input_notes = vec![];
1346 let output_notes: Vec<Note> =
1347 notes_from_output(executed_tx.output_notes()).cloned().collect();
1348 let note_screener = self.note_screener().clone();
1349 let output_note_relevances = note_screener.get_batch_consumability(&output_notes).await?;
1350
1351 for note in output_notes {
1352 if note.script().root() == TxFeeNote::script_root() {
1357 continue;
1358 }
1359
1360 if output_note_relevances.contains_key(¬e.id()) {
1361 let metadata = *note.metadata();
1362 let tag = metadata.tag();
1363 let attachments = note.attachments().clone();
1364
1365 new_input_notes.push(InputNoteRecord::new(
1366 note.into(),
1367 attachments,
1368 current_timestamp,
1369 ExpectedNoteState {
1370 metadata: Some(metadata),
1371 after_block_num: submission_height,
1372 tag: Some(tag),
1373 }
1374 .into(),
1375 ));
1376 }
1377 }
1378
1379 new_input_notes.extend(tx_result.future_notes().iter().map(|(note_details, tag)| {
1381 InputNoteRecord::new(
1382 note_details.clone(),
1383 NoteAttachments::empty(),
1384 None,
1385 ExpectedNoteState {
1386 metadata: None,
1387 after_block_num: current_block_num,
1388 tag: Some(*tag),
1389 }
1390 .into(),
1391 )
1392 }));
1393
1394 let consumed_note_ids =
1399 executed_tx.tx_inputs().input_notes().iter().map(InputNote::id).collect();
1400
1401 let consumed_notes =
1402 self.store.get_input_notes(NoteFilter::List(consumed_note_ids)).await?;
1403
1404 let tracked_note_ids =
1405 consumed_notes.iter().filter_map(InputNoteRecord::id).collect::<BTreeSet<_>>();
1406
1407 for input_note in executed_tx.tx_inputs().input_notes() {
1408 if !tracked_note_ids.contains(&input_note.id()) {
1409 let mut input_note_record = InputNoteRecord::from(input_note.clone());
1410 input_note_record.consumed_locally(
1411 executed_tx.account_id(),
1412 executed_tx.id(),
1413 current_timestamp,
1414 )?;
1415 new_input_notes.push(input_note_record);
1416 }
1417 }
1418
1419 let mut updated_input_notes = vec![];
1420
1421 for mut input_note_record in consumed_notes {
1422 if input_note_record.consumed_locally(
1423 executed_tx.account_id(),
1424 executed_tx.id(),
1425 current_timestamp,
1426 )? {
1427 updated_input_notes.push(input_note_record);
1428 }
1429 }
1430
1431 Ok(NoteUpdateTracker::for_transaction_updates(
1432 new_input_notes,
1433 updated_input_notes,
1434 new_output_notes,
1435 ))
1436 }
1437}
1438
1439#[derive(Debug, thiserror::Error)]
1445pub enum TransactionStoreUpdateError {
1446 #[error("store error")]
1447 Store(#[from] StoreError),
1448 #[error("note screener error")]
1449 NoteScreener(#[from] NoteScreenerError),
1450 #[error("note record error")]
1451 NoteRecord(#[from] NoteRecordError),
1452}
1453
1454#[derive(Clone, Copy, Debug)]
1458enum TransactionExecutionMode {
1459 Standard,
1460 #[cfg(any())]
1461 Dap,
1462}
1463
1464pub(crate) struct PreparedTransaction {
1466 pub(crate) notes: InputNotes<InputNote>,
1467 pub(crate) output_recipients: Vec<NoteRecipient>,
1468 pub(crate) future_notes: Vec<(NoteDetails, NoteTag)>,
1469 pub(crate) tx_args: TransactionArgs,
1470 pub(crate) foreign_account_inputs: Vec<AccountInputs>,
1471 pub(crate) block_num: BlockNumber,
1472 pub(crate) ignore_invalid_notes: bool,
1473}
1474
1475impl PreparedTransaction {
1476 pub(crate) fn output_note_scripts(&self) -> impl Iterator<Item = NoteScript> + '_ {
1479 self.output_recipients.iter().map(|recipient| recipient.script().clone())
1480 }
1481}
1482
1483fn get_outgoing_assets(
1488 transaction_request: &TransactionRequest,
1489) -> (BTreeMap<AccountId, u64>, Vec<Asset>) {
1490 let mut own_notes_assets = match transaction_request.script_template() {
1491 Some(TransactionScriptTemplate::SendNotes(notes)) => notes
1492 .iter()
1493 .map(|note| (note.id(), note.assets().clone()))
1494 .collect::<BTreeMap<_, _>>(),
1495 _ => BTreeMap::default(),
1496 };
1497 let mut output_notes_assets = transaction_request
1498 .expected_output_own_notes()
1499 .into_iter()
1500 .map(|note| (note.id(), note.assets().clone()))
1501 .collect::<BTreeMap<_, _>>();
1502
1503 output_notes_assets.append(&mut own_notes_assets);
1505
1506 let outgoing_assets = output_notes_assets.values().flat_map(|note_assets| note_assets.iter());
1508
1509 request::collect_assets(outgoing_assets)
1510}
1511
1512fn attach_native_fee_conversion_info(
1527 transaction_request: &mut TransactionRequest,
1528 account_code_interface: &AccountCodeInterface,
1529 reference_header: &BlockHeader,
1530 protocol_config: &ProtocolConfig,
1531) -> Result<(), ClientError> {
1532 if transaction_request.has_auth_arg() {
1536 return Ok(());
1537 }
1538
1539 let fee_parameters = reference_header.fee_parameters();
1540 let declared_salt = transaction_request.fee_conversion_salt();
1541 if fee_parameters.verification_base_fee() == 0 && declared_salt.is_none() {
1542 return Ok(());
1543 }
1544
1545 match FeeAuth::of(account_code_interface) {
1546 FeeAuth::FixedSalt => {
1547 transaction_request.commit_native_fee_conversion_info(
1548 protocol_config.fee_asset_id().faucet_id(),
1549 declared_salt.unwrap_or(NATIVE_FEE_CONVERSION_SALT),
1550 );
1551 Ok(())
1552 },
1553 FeeAuth::CallerChosenSalt(component) => match declared_salt {
1554 Some(salt) => {
1555 transaction_request.commit_native_fee_conversion_info(
1556 protocol_config.fee_asset_id().faucet_id(),
1557 salt,
1558 );
1559 Ok(())
1560 },
1561 None => Err(ClientError::TransactionRequestError(
1562 TransactionRequestError::FeeConversionInfoRequired(component),
1563 )),
1564 },
1565 FeeAuth::Ignored(component) => match declared_salt {
1566 Some(_) => Err(ClientError::TransactionRequestError(
1569 TransactionRequestError::FeeConversionInfoUnsupported(component),
1570 )),
1571 None => Ok(()),
1572 },
1573 }
1574}
1575
1576enum FeeAuth {
1578 FixedSalt,
1581 CallerChosenSalt(String),
1584 Ignored(String),
1588}
1589
1590impl FeeAuth {
1591 fn of(account_code_interface: &AccountCodeInterface) -> Self {
1600 let procedures: Vec<_> = account_code_interface.procedures().iter().copied().collect();
1601 let components = AccountComponentInterface::from_procedures(&procedures);
1602
1603 if components
1604 .iter()
1605 .any(|component| matches!(component, AccountComponentInterface::AuthSingleSig))
1606 {
1607 return Self::FixedSalt;
1608 }
1609
1610 let caller_chosen_salt = components.iter().find_map(|component| match component {
1615 AccountComponentInterface::AuthMultisig
1616 | AccountComponentInterface::AuthMultisigSmart
1617 | AccountComponentInterface::AuthGuardedMultisig => {
1618 Some(Self::CallerChosenSalt(component.name()))
1619 },
1620 _ => None,
1621 });
1622
1623 caller_chosen_salt.unwrap_or_else(|| {
1624 let name = components
1625 .iter()
1626 .find(|component| {
1627 matches!(
1628 component,
1629 AccountComponentInterface::AuthNoAuth
1630 | AccountComponentInterface::AuthNetworkAccount
1631 )
1632 })
1633 .map_or_else(|| "unrecognized".into(), AccountComponentInterface::name);
1634
1635 Self::Ignored(name)
1636 })
1637 }
1638}
1639
1640pub(crate) fn native_fee_conversion_info(
1645 account_code_interface: &AccountCodeInterface,
1646 fee_parameters: &FeeParameters,
1647 protocol_config: &ProtocolConfig,
1648) -> Option<FeeConversionInfo> {
1649 if fee_parameters.verification_base_fee() == 0 {
1650 return None;
1651 }
1652
1653 match FeeAuth::of(account_code_interface) {
1656 FeeAuth::FixedSalt => {
1657 Some(FeeConversionInfo::one_to_one(protocol_config.fee_asset_id().faucet_id()))
1658 },
1659 FeeAuth::CallerChosenSalt(_) | FeeAuth::Ignored(_) => None,
1660 }
1661}
1662
1663fn validate_fee_conversion_info_support(
1669 transaction_request: &TransactionRequest,
1670 account_code_interface: &AccountCodeInterface,
1671) -> Result<(), ClientError> {
1672 if transaction_request.fee_conversion_salt().is_none() {
1673 return Ok(());
1674 }
1675
1676 match FeeAuth::of(account_code_interface) {
1677 FeeAuth::FixedSalt | FeeAuth::CallerChosenSalt(_) => Ok(()),
1678 FeeAuth::Ignored(auth_component) => Err(ClientError::TransactionRequestError(
1679 TransactionRequestError::FeeConversionInfoUnsupported(auth_component),
1680 )),
1681 }
1682}
1683fn validate_output_note_senders(
1691 transaction_request: &TransactionRequest,
1692 account_id: AccountId,
1693) -> Result<(), ClientError> {
1694 for note in transaction_request.expected_output_own_notes() {
1695 let sender = note.metadata().sender();
1696 if sender != account_id {
1697 return Err(ClientError::TransactionRequestError(
1698 TransactionRequestError::OutputNoteSenderMismatch {
1699 expected: account_id,
1700 actual: sender,
1701 },
1702 ));
1703 }
1704 }
1705
1706 Ok(())
1707}
1708
1709fn validate_basic_account_request(
1712 transaction_request: &TransactionRequest,
1713 vault_assets: &[Asset],
1714) -> Result<(), ClientError> {
1715 let (fungible_balance_map, non_fungible_set) = get_outgoing_assets(transaction_request);
1716 let (incoming_fungible_balance_map, incoming_non_fungible_balance_set) =
1717 transaction_request.incoming_assets();
1718
1719 let mut available_fungible: BTreeMap<AccountId, u64> = BTreeMap::new();
1722 for asset in vault_assets {
1723 if let Some(fungible) = asset.as_fungible() {
1724 let balance = available_fungible.entry(fungible.faucet_id()).or_default();
1725 *balance = balance.saturating_add(fungible.amount().as_u64());
1726 }
1727 }
1728
1729 for (faucet_id, amount) in fungible_balance_map {
1732 let account_asset_amount = available_fungible.get(&faucet_id).copied().unwrap_or(0);
1733 let incoming_balance = incoming_fungible_balance_map.get(&faucet_id).unwrap_or(&0);
1734 if account_asset_amount + incoming_balance < amount {
1735 return Err(ClientError::AssetError(AssetError::FungibleAssetAmountNotSufficient {
1736 minuend: account_asset_amount,
1737 subtrahend: amount,
1738 }));
1739 }
1740 }
1741
1742 for non_fungible in &non_fungible_set {
1745 let held = vault_assets.iter().any(|asset| asset == non_fungible);
1746 if !held && !incoming_non_fungible_balance_set.contains(non_fungible) {
1747 return Err(ClientError::TransactionRequestError(
1748 TransactionRequestError::MissingNonFungibleAsset(non_fungible.faucet_id()),
1749 ));
1750 }
1751 }
1752
1753 Ok(())
1754}
1755
1756pub(crate) async fn fetch_public_account_inputs(
1766 store: &Arc<dyn Store>,
1767 rpc_api: &Arc<dyn NodeRpcClient>,
1768 account_id: AccountId,
1769 storage_requirements: AccountStorageRequirements,
1770 account_state_at: AccountStateAt,
1771) -> Result<AccountInputs, ClientError> {
1772 let known_code: Option<AccountCode> =
1773 store.get_foreign_account_code(vec![account_id]).await?.into_values().next();
1774
1775 let vault = store
1778 .get_account_header(account_id)
1779 .await?
1780 .map_or(VaultFetch::Always, |(header, ..)| {
1781 VaultFetch::IfChangedFrom(header.vault_root())
1782 });
1783
1784 let (_block_num, account_proof) = rpc_api
1785 .get_account(
1786 account_id,
1787 GetAccountRequest::new()
1788 .with_storage(StorageMapFetch::Slots(storage_requirements.clone()))
1789 .at(account_state_at)
1790 .with_known_code(known_code)
1791 .with_vault(vault),
1792 )
1793 .await?;
1794
1795 let account_inputs = request::account_proof_into_inputs(account_proof)?;
1796
1797 let _ = store
1798 .upsert_foreign_account_code(account_id, account_inputs.code().clone())
1799 .await
1800 .inspect_err(|err| {
1801 tracing::warn!(
1802 %account_id,
1803 %err,
1804 "Failed to persist foreign account code to store"
1805 );
1806 });
1807
1808 Ok(account_inputs)
1809}
1810
1811fn promote_indeterminate_submission(
1814 err: RpcError,
1815 transaction: ProvenTransaction,
1816 transaction_inputs: TransactionInputs,
1817) -> ClientError {
1818 if !err.is_indeterminate_submission() {
1819 return ClientError::RpcError(err);
1820 }
1821
1822 ClientError::SubmissionOutcomeUnknown {
1823 transaction: Box::new(transaction),
1824 transaction_inputs: Box::new(transaction_inputs),
1825 source: err,
1826 }
1827}
1828
1829pub fn notes_from_output(output_notes: &RawOutputNotes) -> impl Iterator<Item = &Note> {
1834 output_notes.iter().filter_map(|n| match n {
1835 RawOutputNote::Full(n) => Some(n),
1836 RawOutputNote::Partial(_) => None,
1837 })
1838}
1839
1840pub(crate) fn validate_executed_transaction(
1843 executed_transaction: &ExecutedTransaction,
1844 expected_output_recipients: &[NoteRecipient],
1845) -> Result<(), ClientError> {
1846 let tx_output_recipient_digests = executed_transaction
1847 .output_notes()
1848 .iter()
1849 .filter_map(|n| n.recipient().map(NoteRecipient::digest))
1850 .collect::<Vec<_>>();
1851
1852 let missing_recipient_digest: Vec<Word> = expected_output_recipients
1853 .iter()
1854 .filter_map(|recipient| {
1855 (!tx_output_recipient_digests.contains(&recipient.digest()))
1856 .then_some(recipient.digest())
1857 })
1858 .collect();
1859
1860 if !missing_recipient_digest.is_empty() {
1861 return Err(ClientError::MissingOutputRecipients(missing_recipient_digest));
1862 }
1863
1864 Ok(())
1865}
1866
1867#[cfg(test)]
1871mod tests {
1872 use alloc::vec;
1873
1874 use miden_protocol::Word;
1875 use miden_protocol::account::auth::AuthSecretKey;
1876 use miden_protocol::account::{
1877 Account,
1878 AccountBuilder,
1879 AccountComponent,
1880 AccountComponentMetadata,
1881 AccountId,
1882 AccountType,
1883 };
1884 use miden_protocol::asset::{AssetId, FungibleAsset};
1885 use miden_protocol::block::{BlockHeader, BlockNumber, FeeParameters};
1886 use miden_protocol::crypto::rand::RandomCoin;
1887 use miden_protocol::note::{Note, NoteType};
1888 use miden_protocol::protocol_config::ProtocolConfig;
1889 use miden_protocol::testing::account_id::{
1890 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
1891 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
1892 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
1893 ACCOUNT_ID_SENDER,
1894 };
1895 use miden_standards::account::AccountBuilderSchemaCommitmentExt;
1896 use miden_standards::account::auth::{
1897 Approver,
1898 ApproverSet,
1899 AuthGuardedMultisig,
1900 AuthGuardedMultisigConfig,
1901 AuthMultisig,
1902 AuthMultisigConfig,
1903 AuthMultisigSmart,
1904 AuthMultisigSmartConfig,
1905 AuthSingleSig,
1906 FeeConversionInfo,
1907 GuardianConfig,
1908 NoAuth,
1909 commit_fee_conversion_info,
1910 };
1911 use miden_standards::account::wallets::BasicWallet;
1912 use miden_standards::note::P2idNote;
1913
1914 use super::{
1915 AccountComponentInterface,
1916 NATIVE_FEE_CONVERSION_SALT,
1917 TransactionRequest,
1918 TransactionRequestBuilder,
1919 attach_native_fee_conversion_info,
1920 validate_fee_conversion_info_support,
1921 validate_output_note_senders,
1922 };
1923 use crate::ClientError;
1924 use crate::assembly::CodeBuilder;
1925 use crate::auth::AuthSchemeId;
1926 use crate::transaction::TransactionRequestError;
1927
1928 fn own_note_with_sender(sender: AccountId) -> Note {
1929 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
1930 let target_id =
1931 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1932 let mut rng = RandomCoin::new(Word::default());
1933
1934 P2idNote::builder()
1935 .sender(sender)
1936 .target(target_id)
1937 .asset(FungibleAsset::new(faucet_id, 100).unwrap())
1938 .note_type(NoteType::Public)
1939 .generate_serial_number(&mut rng)
1940 .build()
1941 .expect("note creation failed")
1942 .into()
1943 }
1944
1945 #[test]
1946 fn output_note_with_foreign_sender_is_rejected() {
1947 let account_id =
1948 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1949 let foreign_sender = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
1950 assert_ne!(account_id, foreign_sender);
1951
1952 let request = TransactionRequestBuilder::new()
1953 .own_output_notes(vec![own_note_with_sender(foreign_sender)])
1954 .build()
1955 .unwrap();
1956
1957 let err = validate_output_note_senders(&request, account_id).unwrap_err();
1958 match err {
1959 ClientError::TransactionRequestError(
1960 TransactionRequestError::OutputNoteSenderMismatch { expected, actual },
1961 ) => {
1962 assert_eq!(expected, account_id);
1963 assert_eq!(actual, foreign_sender);
1964 },
1965 other => panic!("expected OutputNoteSenderMismatch, got {other:?}"),
1966 }
1967 }
1968
1969 #[test]
1970 fn output_note_with_matching_sender_is_accepted() {
1971 let account_id =
1972 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1973
1974 let request = TransactionRequestBuilder::new()
1975 .own_output_notes(vec![own_note_with_sender(account_id)])
1976 .build()
1977 .unwrap();
1978
1979 validate_output_note_senders(&request, account_id).unwrap();
1980 }
1981
1982 #[test]
1983 fn request_without_own_output_notes_is_accepted() {
1984 let account_id =
1985 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1986 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
1987
1988 let request = TransactionRequestBuilder::new()
1990 .input_notes(vec![(own_note_with_sender(faucet_id), None)])
1991 .build()
1992 .unwrap();
1993
1994 validate_output_note_senders(&request, account_id).unwrap();
1995 }
1996
1997 fn account_with_auth(auth_component: impl Into<AccountComponent>) -> Account {
1999 AccountBuilder::new([7u8; 32])
2000 .account_type(AccountType::Public)
2001 .with_component(auth_component)
2002 .with_component(BasicWallet)
2003 .build_with_schema_commitment()
2004 .expect("account creation failed")
2005 }
2006
2007 fn fee_conversion_request() -> TransactionRequest {
2008 TransactionRequestBuilder::new()
2009 .fee_conversion_salt(Word::from([13u32, 14, 15, 16]))
2010 .build()
2011 .unwrap()
2012 }
2013
2014 #[test]
2015 fn fee_conversion_info_is_accepted_by_a_signature_authenticated_account() {
2016 let key = AuthSecretKey::new_falcon512_poseidon2();
2017 let auth = AuthSingleSig::new(Approver::new(
2018 key.public_key().to_commitment(),
2019 AuthSchemeId::Falcon512Poseidon2,
2020 ));
2021
2022 validate_fee_conversion_info_support(
2023 &fee_conversion_request(),
2024 &account_with_auth(auth).code_interface(),
2025 )
2026 .unwrap();
2027 }
2028
2029 #[test]
2030 fn fee_conversion_info_is_rejected_by_an_account_that_cannot_read_it() {
2031 let account = account_with_auth(NoAuth);
2032
2033 let err = validate_fee_conversion_info_support(
2034 &fee_conversion_request(),
2035 &account.code_interface(),
2036 )
2037 .expect_err("NoAuth does not read the auth args");
2038 match err {
2039 ClientError::TransactionRequestError(
2040 TransactionRequestError::FeeConversionInfoUnsupported(auth_component),
2041 ) => assert_eq!(auth_component, AccountComponentInterface::AuthNoAuth.name()),
2042 other => panic!("expected FeeConversionInfoUnsupported, got {other:?}"),
2043 }
2044 }
2045
2046 #[test]
2047 fn a_request_without_fee_conversion_info_skips_the_auth_component_check() {
2048 validate_fee_conversion_info_support(
2050 &TransactionRequestBuilder::new().build().unwrap(),
2051 &account_with_auth(NoAuth).code_interface(),
2052 )
2053 .unwrap();
2054 }
2055
2056 const NATIVE_FEE_FAUCET: u128 = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET;
2062
2063 fn test_protocol_config() -> ProtocolConfig {
2064 ProtocolConfig::current(AssetId::new_fungible(NATIVE_FEE_FAUCET.try_into().unwrap()))
2065 .unwrap()
2066 }
2067
2068 fn header_with_base_fee(verification_base_fee: u32) -> BlockHeader {
2070 let fee_parameters = FeeParameters::new(verification_base_fee);
2071 let (_, validator_keys) = miden_protocol::block::ValidatorConfig::random_with_signers(1);
2072
2073 BlockHeader::new(
2074 Word::empty(),
2075 BlockNumber::from(1u32),
2076 Word::empty(),
2077 Word::empty(),
2078 Word::empty(),
2079 Word::empty(),
2080 Word::empty(),
2081 validator_keys,
2082 fee_parameters,
2083 test_protocol_config().to_commitment(),
2084 None,
2085 0,
2086 )
2087 }
2088
2089 fn injected_auth_arg(
2092 mut request: TransactionRequest,
2093 account: &Account,
2094 verification_base_fee: u32,
2095 ) -> Option<Word> {
2096 let _ = attach_native_fee_conversion_info(
2097 &mut request,
2098 &account.code_interface(),
2099 &header_with_base_fee(verification_base_fee),
2100 &test_protocol_config(),
2101 );
2102 *request.auth_arg()
2103 }
2104
2105 fn try_injected_auth_arg(
2107 mut request: TransactionRequest,
2108 account: &Account,
2109 verification_base_fee: u32,
2110 ) -> Result<Option<Word>, ClientError> {
2111 attach_native_fee_conversion_info(
2112 &mut request,
2113 &account.code_interface(),
2114 &header_with_base_fee(verification_base_fee),
2115 &test_protocol_config(),
2116 )?;
2117 Ok(*request.auth_arg())
2118 }
2119
2120 fn singlesig_account() -> Account {
2121 let key = AuthSecretKey::new_falcon512_poseidon2();
2122 account_with_auth(AuthSingleSig::new(Approver::new(
2123 key.public_key().to_commitment(),
2124 AuthSchemeId::Falcon512Poseidon2,
2125 )))
2126 }
2127
2128 fn guarded_multisig_account() -> Account {
2129 let approvers = ApproverSet::new(
2130 vec![Approver::new(
2131 AuthSecretKey::new_falcon512_poseidon2().public_key().to_commitment(),
2132 AuthSchemeId::Falcon512Poseidon2,
2133 )],
2134 1,
2135 )
2136 .unwrap();
2137
2138 let guardian = GuardianConfig::new(Approver::new(
2139 AuthSecretKey::new_falcon512_poseidon2().public_key().to_commitment(),
2140 AuthSchemeId::Falcon512Poseidon2,
2141 ));
2142
2143 account_with_auth(
2144 AuthGuardedMultisig::new(AuthGuardedMultisigConfig::new(approvers, guardian).unwrap())
2145 .unwrap(),
2146 )
2147 }
2148
2149 #[test]
2150 fn native_fee_conversion_info_is_attached_on_a_fee_charging_chain() {
2151 let auth_arg = injected_auth_arg(
2152 TransactionRequestBuilder::new().build().unwrap(),
2153 &singlesig_account(),
2154 500,
2155 )
2156 .expect("a fee-charging chain should get conversion info attached");
2157
2158 let (expected, _) = commit_fee_conversion_info(
2159 FeeConversionInfo::one_to_one(AccountId::try_from(NATIVE_FEE_FAUCET).unwrap()),
2160 NATIVE_FEE_CONVERSION_SALT,
2161 );
2162 assert_eq!(auth_arg, expected, "the fee should be paid in the native asset at rate 1/1");
2163 }
2164
2165 #[test]
2166 fn an_explicit_auth_arg_is_not_overwritten() {
2167 let auth_arg = Word::from([21u32, 22, 23, 24]);
2168 let request = TransactionRequestBuilder::new().auth_arg(auth_arg).build().unwrap();
2169
2170 assert_eq!(
2171 injected_auth_arg(request, &singlesig_account(), 500),
2172 Some(auth_arg),
2173 "a request that declares its own auth arg keeps it"
2174 );
2175 }
2176
2177 fn native_commitment(salt: Word) -> Word {
2179 let (auth_arg, _) = commit_fee_conversion_info(
2180 FeeConversionInfo::one_to_one(AccountId::try_from(NATIVE_FEE_FAUCET).unwrap()),
2181 salt,
2182 );
2183 auth_arg
2184 }
2185
2186 #[test]
2187 fn a_declared_salt_is_used_for_the_native_commitment() {
2188 let salt = Word::from([17u32, 18, 19, 20]);
2189 let request = TransactionRequestBuilder::new().fee_conversion_salt(salt).build().unwrap();
2190
2191 let auth_arg = injected_auth_arg(request, &singlesig_account(), 500)
2192 .expect("a declared salt should still get native conversion info attached");
2193
2194 assert_eq!(auth_arg, native_commitment(salt));
2195 }
2196
2197 fn multisig_account() -> Account {
2198 let approvers = ApproverSet::new(
2199 vec![Approver::new(
2200 AuthSecretKey::new_falcon512_poseidon2().public_key().to_commitment(),
2201 AuthSchemeId::Falcon512Poseidon2,
2202 )],
2203 1,
2204 )
2205 .unwrap();
2206
2207 account_with_auth(AuthMultisig::new(AuthMultisigConfig::new(approvers)).unwrap())
2208 }
2209
2210 #[test]
2211 fn nothing_is_attached_where_it_is_not_needed_or_not_readable() {
2212 for (case, account, base_fee) in [
2213 ("a zero base fee charges nothing", singlesig_account(), 0),
2214 ("NoAuth never reads the auth args", account_with_auth(NoAuth), 500),
2215 ("a multisig salt is its own replay guard", multisig_account(), 500),
2216 ] {
2217 assert_eq!(
2218 injected_auth_arg(
2219 TransactionRequestBuilder::new().build().unwrap(),
2220 &account,
2221 base_fee
2222 ),
2223 None,
2224 "{case}"
2225 );
2226 }
2227 }
2228
2229 #[test]
2236 fn fee_conversion_info_is_accepted_by_a_guarded_multisig_account() {
2237 validate_fee_conversion_info_support(
2238 &fee_conversion_request(),
2239 &guarded_multisig_account().code_interface(),
2240 )
2241 .expect("a guarded multisig reads the auth args as conversion info");
2242 }
2243
2244 #[test]
2248 fn a_guarded_multisig_account_must_declare_its_own_fee_conversion_info() {
2249 let err = try_injected_auth_arg(
2250 TransactionRequestBuilder::new().build().unwrap(),
2251 &guarded_multisig_account(),
2252 500,
2253 )
2254 .expect_err("a guarded multisig account cannot inherit the fixed native salt");
2255 match err {
2256 ClientError::TransactionRequestError(
2257 TransactionRequestError::FeeConversionInfoRequired(auth_component),
2258 ) => {
2259 assert_eq!(auth_component, AccountComponentInterface::AuthGuardedMultisig.name());
2260 },
2261 other => panic!("expected FeeConversionInfoRequired, got {other:?}"),
2262 }
2263
2264 let salt = Word::from([13u32, 14, 15, 16]);
2265 assert_eq!(
2266 try_injected_auth_arg(fee_conversion_request(), &guarded_multisig_account(), 500)
2267 .expect("a declared salt is accepted"),
2268 Some(native_commitment(salt)),
2269 "a guarded multisig account that declares a salt commits the native conversion info"
2270 );
2271
2272 assert_eq!(
2273 try_injected_auth_arg(
2274 TransactionRequestBuilder::new().build().unwrap(),
2275 &guarded_multisig_account(),
2276 0
2277 )
2278 .expect("a chain charging nothing needs no conversion info"),
2279 None,
2280 );
2281 }
2282
2283 fn smart_multisig_account() -> Account {
2287 let approvers = ApproverSet::new(
2288 vec![Approver::new(
2289 AuthSecretKey::new_falcon512_poseidon2().public_key().to_commitment(),
2290 AuthSchemeId::Falcon512Poseidon2,
2291 )],
2292 1,
2293 )
2294 .unwrap();
2295
2296 account_with_auth(AuthMultisigSmart::new(AuthMultisigSmartConfig::new(approvers)).unwrap())
2297 }
2298
2299 #[test]
2304 fn fee_conversion_info_is_accepted_by_a_smart_multisig_account() {
2305 validate_fee_conversion_info_support(
2306 &fee_conversion_request(),
2307 &smart_multisig_account().code_interface(),
2308 )
2309 .expect("a smart multisig reads the auth args as conversion info");
2310 }
2311
2312 #[test]
2316 fn a_smart_multisig_account_must_declare_its_own_fee_conversion_info() {
2317 let err = try_injected_auth_arg(
2318 TransactionRequestBuilder::new().build().unwrap(),
2319 &smart_multisig_account(),
2320 500,
2321 )
2322 .expect_err("a smart multisig account cannot inherit the fixed native salt");
2323 match err {
2324 ClientError::TransactionRequestError(
2325 TransactionRequestError::FeeConversionInfoRequired(auth_component),
2326 ) => {
2327 assert_eq!(auth_component, AccountComponentInterface::AuthMultisigSmart.name());
2328 },
2329 other => panic!("expected FeeConversionInfoRequired, got {other:?}"),
2330 }
2331
2332 let salt = Word::from([13u32, 14, 15, 16]);
2333 assert_eq!(
2334 try_injected_auth_arg(fee_conversion_request(), &smart_multisig_account(), 500)
2335 .expect("a declared salt is accepted"),
2336 Some(native_commitment(salt)),
2337 "a smart multisig account that declares a salt commits the native conversion info"
2338 );
2339
2340 assert_eq!(
2341 try_injected_auth_arg(
2342 TransactionRequestBuilder::new().build().unwrap(),
2343 &smart_multisig_account(),
2344 0
2345 )
2346 .expect("a chain charging nothing needs no conversion info"),
2347 None,
2348 );
2349 }
2350
2351 #[test]
2354 fn an_unrecognized_auth_component_is_rejected_rather_than_panicking() {
2355 const CUSTOM_AUTH: &str = "
2356 use miden::protocol::native_account
2357
2358 @auth_script
2359 pub proc auth_custom
2360 exec.native_account::incr_nonce
2361 drop
2362 end
2363 ";
2364
2365 let code = CodeBuilder::default()
2366 .compile_component_code("miden::testing::custom_auth", CUSTOM_AUTH)
2367 .expect("custom auth component code should compile");
2368 let auth = AccountComponent::new(
2369 code,
2370 vec![],
2371 AccountComponentMetadata::new("miden::testing::custom_auth"),
2372 )
2373 .expect("custom auth component");
2374
2375 let account = account_with_auth(auth);
2376
2377 let err = validate_fee_conversion_info_support(
2378 &fee_conversion_request(),
2379 &account.code_interface(),
2380 )
2381 .expect_err("an account with no recognized auth component cannot read conversion info");
2382 assert!(matches!(
2383 err,
2384 ClientError::TransactionRequestError(
2385 TransactionRequestError::FeeConversionInfoUnsupported(_)
2386 )
2387 ));
2388
2389 assert_eq!(
2390 try_injected_auth_arg(TransactionRequestBuilder::new().build().unwrap(), &account, 500)
2391 .expect("a request declaring nothing is left alone"),
2392 None,
2393 "an auth component nothing can reason about gets nothing attached"
2394 );
2395 }
2396}