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