1use alloc::boxed::Box;
66use alloc::collections::{BTreeMap, BTreeSet};
67use alloc::string::String;
68use alloc::sync::Arc;
69use alloc::vec::Vec;
70
71use miden_protocol::account::{AccountCode, AccountCodeInterface, AccountId, PartialAccount};
72use miden_protocol::asset::{Asset, NonFungibleAsset};
73use miden_protocol::block::{BlockHeader, BlockNumber, FeeParameters};
74use miden_protocol::errors::AssetError;
75use miden_protocol::note::{
76 Note,
77 NoteAttachments,
78 NoteDetails,
79 NoteId,
80 NoteRecipient,
81 NoteScript,
82 NoteTag,
83};
84use miden_protocol::transaction::{AccountInputs, PartialBlockchain};
85use miden_protocol::vm::MIN_STACK_DEPTH;
86use miden_protocol::{Felt, Word};
87use miden_standards::account::auth::FeeConversionInfo;
88use miden_standards::account::faucets::FungibleFaucet;
89use miden_standards::account::interface::AccountComponentInterfaceExt;
90use miden_standards::note::TxFeeNote;
91use miden_tx::{DataStore, NoteConsumptionChecker, TransactionExecutor};
92use tracing::info;
93
94use super::Client;
95use crate::ClientError;
96use crate::note::{NoteScreenerError, NoteUpdateTracker, StandardNote};
97use crate::rpc::domain::account::{
98 AccountStorageRequirements,
99 GetAccountRequest,
100 StorageMapFetch,
101 VaultFetch,
102};
103use crate::rpc::encryption::{TransactionEncryptionKey, seal_transaction_inputs};
104use crate::rpc::{AccountStateAt, NodeRpcClient, RpcError};
105use crate::store::data_store::{ClientDataStore, build_partial_mmr_with_paths};
106use crate::store::input_note_states::ExpectedNoteState;
107use crate::store::{
108 AccountRecord,
109 InputNoteRecord,
110 InputNoteState,
111 NoteFilter,
112 NoteRecordError,
113 OutputNoteRecord,
114 Store,
115 StoreError,
116 TransactionFilter,
117};
118use crate::sync::NoteTagRecord;
119use crate::transaction::batch::InMemoryBatchDataStore;
120
121pub mod batch;
122pub use batch::{BatchBuilder, BatchBuilderError};
123
124mod chain_anchor;
125pub use chain_anchor::{ChainAnchor, ChainAnchorError};
126
127#[cfg(feature = "dap")]
128mod dap_executor;
129mod prover;
130pub use prover::TransactionProver;
131
132mod record;
133pub use record::{
134 DiscardCause,
135 TransactionDetails,
136 TransactionRecord,
137 TransactionStatus,
138 TransactionStatusVariant,
139};
140
141mod store_update;
142pub use store_update::TransactionStoreUpdate;
143
144mod request;
145pub use request::{
146 ForeignAccount,
147 NoteArgs,
148 PaymentNoteDescription,
149 PswapTransactionData,
150 SwapTransactionData,
151 TransactionRequest,
152 TransactionRequestBuilder,
153 TransactionRequestError,
154 TransactionScriptTemplate,
155 build_fpi_script,
156};
157
158mod observer;
159pub use observer::TransactionObserver;
160
161mod result;
162pub use miden_protocol::transaction::{
165 ExecutedTransaction,
166 InputNote,
167 InputNotes,
168 OutputNote,
169 OutputNotes,
170 ProvenTransaction,
171 PublicOutputNote,
172 RawOutputNote,
173 RawOutputNotes,
174 TransactionArgs,
175 TransactionId,
176 TransactionInputs,
177 TransactionKernel,
178 TransactionScript,
179 TransactionScriptRoot,
180 TransactionSummary,
181};
182pub use miden_protocol::vm::{AdviceInputs, AdviceMap};
183pub use miden_standards::account::interface::{AccountComponentInterface, AccountInterface};
184pub use miden_standards::tx_script::{
185 ExpirationTransactionScript,
186 SendNotesTransactionScriptError,
187};
188pub use miden_tx::auth::TransactionAuthenticator;
189pub use miden_tx::{
190 DataStoreError,
191 LocalTransactionProver,
192 ProvingOptions,
193 TransactionExecutorError,
194 TransactionProverError,
195};
196pub use result::TransactionResult;
197
198pub(crate) const NATIVE_FEE_CONVERSION_SALT: Word = Word::empty();
205
206impl<AUTH> Client<AUTH>
208where
209 AUTH: TransactionAuthenticator + Sync + 'static,
210{
211 pub async fn get_transactions(
216 &self,
217 filter: TransactionFilter,
218 ) -> Result<Vec<TransactionRecord>, ClientError> {
219 self.store.get_transactions(filter).await.map_err(Into::into)
220 }
221
222 pub fn new_transaction_batch(&mut self) -> BatchBuilder<'_, AUTH> {
230 let inner_data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
231 BatchBuilder {
232 client: self,
233 data_store: InMemoryBatchDataStore::new(inner_data_store),
234 pushed_txs: Vec::new(),
235 consumed_input_notes: BTreeSet::new(),
236 }
237 }
238
239 pub async fn submit_new_transaction(
248 &mut self,
249 account_id: AccountId,
250 transaction_request: TransactionRequest,
251 ) -> Result<TransactionId, ClientError> {
252 let prover = self.tx_prover.clone();
253 self.submit_new_transaction_with_prover(account_id, transaction_request, prover)
254 .await
255 }
256
257 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 =
296 Box::new(self.get_transaction_store_update(&tx_result, submission_height).await?);
297
298 if let Err(apply_err) = self.apply_transaction_update((*tx_update).clone()).await {
299 info!(
300 "apply_transaction_update failed for submitted tx {tx_id}; returning \
301 ApplyTransactionAfterSubmitFailed with the pending update attached: {apply_err}"
302 );
303 return Err(ClientError::ApplyTransactionAfterSubmitFailed {
304 pending_update: tx_update,
305 source: Box::new(apply_err),
306 });
307 }
308
309 for observer in &self.transaction_observers {
313 crate::errors::log_observer_failure(
314 observer.name(),
315 "TransactionObserver::apply",
316 observer.apply(&tx_result).await,
317 );
318 }
319
320 Ok(tx_id)
321 }
322
323 pub async fn execute_transaction(
333 &self,
334 account_id: AccountId,
335 transaction_request: TransactionRequest,
336 ) -> Result<TransactionResult, ClientError> {
337 self.execute_transaction_with_mode(
338 account_id,
339 transaction_request,
340 TransactionExecutionMode::Standard,
341 None,
342 )
343 .await
344 }
345
346 pub async fn execute_transaction_at(
379 &mut self,
380 account_id: AccountId,
381 transaction_request: TransactionRequest,
382 anchor: ChainAnchor,
383 ) -> Result<TransactionResult, ClientError> {
384 let result = self
385 .execute_transaction_with_mode(
386 account_id,
387 transaction_request,
388 TransactionExecutionMode::Standard,
389 Some(Box::new(anchor)),
390 )
391 .await?;
392
393 let expiration = result.executed_transaction().expiration_block_num();
398 let sync_height = self.store.get_sync_height().await?;
399 if expiration <= sync_height {
400 return Err(
401 ChainAnchorError::AnchoredTransactionExpired { expiration, sync_height }.into()
402 );
403 }
404
405 Ok(result)
406 }
407
408 async fn chain_anchor_at_tip(
413 &self,
414 tracked_blocks: BTreeSet<BlockNumber>,
415 ) -> Result<ChainAnchor, ClientError> {
416 let sync_height = self.store.get_sync_height().await?;
417
418 let (header, _had_notes) = self
419 .store
420 .get_block_header_by_num(sync_height)
421 .await?
422 .ok_or(StoreError::BlockHeaderNotFound(sync_height))?;
423
424 let mut tracked_blocks = tracked_blocks;
425 tracked_blocks.remove(&sync_height);
427
428 let block_headers: Vec<BlockHeader> = self
429 .store
430 .get_block_headers(&tracked_blocks)
431 .await?
432 .into_iter()
433 .map(|(header, _has_notes)| header)
434 .collect();
435
436 let fetched_nums: BTreeSet<BlockNumber> =
439 block_headers.iter().map(BlockHeader::block_num).collect();
440 if let Some(&missing) = tracked_blocks.difference(&fetched_nums).next() {
441 return Err(StoreError::BlockHeaderNotFound(missing).into());
442 }
443
444 let peaks = self.store.get_current_blockchain_peaks().await?;
445 let partial_mmr = build_partial_mmr_with_paths(&self.store, peaks, &block_headers).await?;
446
447 let chain = PartialBlockchain::new(partial_mmr, block_headers)?;
448
449 Ok(ChainAnchor::new(header, chain)?)
450 }
451
452 pub async fn chain_anchor_for_request(
471 &self,
472 transaction_request: &TransactionRequest,
473 ) -> Result<ChainAnchor, ClientError> {
474 let inferred_input_note_ids: Vec<NoteId> = transaction_request
475 .input_note_ids()
476 .filter(|note_id| !transaction_request.explicit_input_notes.contains_key(note_id))
477 .collect();
478
479 let mut tracked_blocks: BTreeSet<BlockNumber> = if inferred_input_note_ids.is_empty() {
480 BTreeSet::new()
481 } else {
482 self.store
483 .get_input_notes(NoteFilter::List(inferred_input_note_ids))
484 .await?
485 .iter()
486 .filter(|record| record.is_authenticated())
487 .filter_map(|record| record.inclusion_proof())
488 .map(|proof| proof.location().block_num())
489 .collect()
490 };
491 tracked_blocks.extend(
492 transaction_request
493 .explicit_input_notes
494 .values()
495 .filter_map(InputNote::proof)
496 .map(|proof| proof.location().block_num()),
497 );
498
499 self.chain_anchor_at_tip(tracked_blocks).await
500 }
501
502 #[cfg(feature = "dap")]
516 pub async fn execute_transaction_with_dap(
517 &self,
518 account_id: AccountId,
519 transaction_request: TransactionRequest,
520 ) -> Result<TransactionResult, ClientError> {
521 self.execute_transaction_with_mode(
522 account_id,
523 transaction_request,
524 TransactionExecutionMode::Dap,
525 None,
526 )
527 .await
528 }
529
530 async fn execute_transaction_with_mode(
534 &self,
535 account_id: AccountId,
536 transaction_request: TransactionRequest,
537 execution_mode: TransactionExecutionMode,
538 anchor: Option<Box<ChainAnchor>>,
539 ) -> Result<TransactionResult, ClientError> {
540 let account: PartialAccount =
541 self.get_native_account_record(account_id).await?.try_into()?;
542
543 let prep = self
544 .prepare_transaction(&account, transaction_request, anchor.as_deref())
545 .await?;
546
547 let mut data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
548 if let Some(anchor) = anchor {
549 data_store = data_store.with_chain_anchor(*anchor);
550 }
551 data_store.register_note_scripts(prep.output_note_scripts());
552 for fpi_account in &prep.foreign_account_inputs {
553 data_store.mast_store().load_account_code(fpi_account.code());
554 }
555 data_store.register_foreign_account_inputs(prep.foreign_account_inputs);
556
557 data_store.mast_store().load_account_code(account.code());
558
559 let mut notes = prep.notes;
560 if prep.ignore_invalid_notes {
561 notes = self
562 .get_valid_input_notes(
563 &data_store,
564 account.id(),
565 prep.block_num,
566 notes,
567 prep.tx_args.clone(),
568 )
569 .await?;
570 }
571
572 let executed_transaction = match execution_mode {
573 TransactionExecutionMode::Standard => {
574 self.build_executor(&data_store)?
575 .execute_transaction(account_id, prep.block_num, notes, prep.tx_args)
576 .await?
577 },
578 #[cfg(feature = "dap")]
579 TransactionExecutionMode::Dap => {
580 self.build_dap_executor(&data_store)?
581 .execute_transaction(account_id, prep.block_num, notes, prep.tx_args)
582 .await?
583 },
584 };
585
586 validate_executed_transaction(&executed_transaction, &prep.output_recipients)?;
587 TransactionResult::new(executed_transaction, prep.future_notes)
588 }
589
590 pub(crate) async fn prepare_transaction(
606 &self,
607 account: &PartialAccount,
608 transaction_request: TransactionRequest,
609 anchor: Option<&ChainAnchor>,
610 ) -> Result<PreparedTransaction, ClientError> {
611 self.validate_account_request(
612 &transaction_request,
613 account.id(),
614 &account.code_interface(),
615 )
616 .await?;
617
618 self.prepare_transaction_inner(account.code_interface(), transaction_request, anchor)
619 .await
620 }
621
622 pub(crate) async fn prepare_transaction_for_batch(
623 &self,
624 account: &PartialAccount,
625 transaction_request: TransactionRequest,
626 ) -> Result<PreparedTransaction, ClientError> {
627 self.prepare_transaction_inner(account.code_interface(), transaction_request, None)
628 .await
629 }
630
631 async fn prepare_transaction_inner(
632 &self,
633 account_code_interface: AccountCodeInterface,
634 mut transaction_request: TransactionRequest,
635 anchor: Option<&ChainAnchor>,
636 ) -> Result<PreparedTransaction, ClientError> {
637 if anchor.is_none() {
638 self.validate_recency().await?;
639 }
640
641 let mut stored_note_records = self
643 .store
644 .get_input_notes(NoteFilter::List(transaction_request.input_note_ids().collect()))
645 .await?;
646
647 for note in &stored_note_records {
649 if note.is_consumed() {
650 return Err(ClientError::TransactionRequestError(
651 TransactionRequestError::InputNoteAlreadyConsumed(note.details_commitment()),
652 ));
653 }
654 }
655
656 stored_note_records.retain(InputNoteRecord::is_authenticated);
658
659 let notes = transaction_request.build_input_notes(stored_note_records)?;
660
661 if let Some(anchor) = anchor {
665 for note in notes.iter() {
666 if let Some(location) = note.location() {
667 let block_num = location.block_num();
668 if block_num < anchor.block_num()
669 && !anchor.partial_blockchain().contains_block(block_num)
670 {
671 return Err(ChainAnchorError::BlockNotTracked { block_num }.into());
672 }
673 }
674 }
675 }
676
677 let output_recipients =
678 transaction_request.expected_output_recipients().cloned().collect::<Vec<_>>();
679
680 let future_notes: Vec<(NoteDetails, NoteTag)> =
681 transaction_request.expected_future_notes().cloned().collect();
682
683 let tx_script = transaction_request.build_transaction_script(&account_code_interface)?;
684
685 let foreign_accounts = transaction_request.foreign_accounts().clone();
686
687 let block_num = match anchor {
690 Some(anchor) => anchor.block_num(),
691 None => self.store.get_sync_height().await?,
692 };
693
694 let foreign_account_inputs =
695 self.retrieve_foreign_account_inputs(foreign_accounts, block_num).await?;
696
697 let ignore_invalid_notes = transaction_request.ignore_invalid_input_notes();
698
699 let reference_header = match anchor {
700 Some(anchor) => anchor.header().clone(),
701 None => {
702 self.store
703 .get_block_header_by_num(block_num)
704 .await?
705 .ok_or(StoreError::BlockHeaderNotFound(block_num))?
706 .0
707 },
708 };
709 attach_native_fee_conversion_info(
710 &mut transaction_request,
711 &account_code_interface,
712 &reference_header,
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_keys().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(feature = "dap")]
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(feature = "dap")]
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(feature = "dap")]
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<NonFungibleAsset>) {
1490 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
1500 .expected_output_own_notes()
1501 .into_iter()
1502 .map(|note| (note.id(), note.assets().clone()))
1503 .collect::<BTreeMap<_, _>>();
1504
1505 output_notes_assets.append(&mut own_notes_assets);
1507
1508 let outgoing_assets = output_notes_assets.values().flat_map(|note_assets| note_assets.iter());
1510
1511 request::collect_assets(outgoing_assets)
1512}
1513
1514fn attach_native_fee_conversion_info(
1529 transaction_request: &mut TransactionRequest,
1530 account_code_interface: &AccountCodeInterface,
1531 reference_header: &BlockHeader,
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 fee_parameters.fee_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
1557 .commit_native_fee_conversion_info(fee_parameters.fee_faucet_id(), salt);
1558 Ok(())
1559 },
1560 None => Err(ClientError::TransactionRequestError(
1561 TransactionRequestError::FeeConversionInfoRequired(component),
1562 )),
1563 },
1564 FeeAuth::Ignored(component) => match declared_salt {
1565 Some(_) => Err(ClientError::TransactionRequestError(
1568 TransactionRequestError::FeeConversionInfoUnsupported(component),
1569 )),
1570 None => Ok(()),
1571 },
1572 }
1573}
1574
1575enum FeeAuth {
1577 FixedSalt,
1580 CallerChosenSalt(String),
1583 Ignored(String),
1587}
1588
1589impl FeeAuth {
1590 fn of(account_code_interface: &AccountCodeInterface) -> Self {
1599 let procedures: Vec<_> = account_code_interface.procedures().iter().copied().collect();
1600 let components = AccountComponentInterface::from_procedures(&procedures);
1601
1602 if components
1603 .iter()
1604 .any(|component| matches!(component, AccountComponentInterface::AuthSingleSig))
1605 {
1606 return Self::FixedSalt;
1607 }
1608
1609 let caller_chosen_salt = components.iter().find_map(|component| match component {
1614 AccountComponentInterface::AuthMultisig
1615 | AccountComponentInterface::AuthMultisigSmart
1616 | AccountComponentInterface::AuthGuardedMultisig => {
1617 Some(Self::CallerChosenSalt(component.name()))
1618 },
1619 _ => None,
1620 });
1621
1622 caller_chosen_salt.unwrap_or_else(|| {
1623 let name = components
1624 .iter()
1625 .find(|component| {
1626 matches!(
1627 component,
1628 AccountComponentInterface::AuthNoAuth
1629 | AccountComponentInterface::AuthNetworkAccount
1630 )
1631 })
1632 .map_or_else(|| "unrecognized".into(), AccountComponentInterface::name);
1633
1634 Self::Ignored(name)
1635 })
1636 }
1637}
1638
1639pub(crate) fn native_fee_conversion_info(
1644 account_code_interface: &AccountCodeInterface,
1645 fee_parameters: &FeeParameters,
1646) -> Option<FeeConversionInfo> {
1647 if fee_parameters.verification_base_fee() == 0 {
1648 return None;
1649 }
1650
1651 match FeeAuth::of(account_code_interface) {
1654 FeeAuth::FixedSalt => Some(FeeConversionInfo::one_to_one(fee_parameters.fee_faucet_id())),
1655 FeeAuth::CallerChosenSalt(_) | FeeAuth::Ignored(_) => None,
1656 }
1657}
1658
1659fn validate_fee_conversion_info_support(
1665 transaction_request: &TransactionRequest,
1666 account_code_interface: &AccountCodeInterface,
1667) -> Result<(), ClientError> {
1668 if transaction_request.fee_conversion_salt().is_none() {
1669 return Ok(());
1670 }
1671
1672 match FeeAuth::of(account_code_interface) {
1673 FeeAuth::FixedSalt | FeeAuth::CallerChosenSalt(_) => Ok(()),
1674 FeeAuth::Ignored(auth_component) => Err(ClientError::TransactionRequestError(
1675 TransactionRequestError::FeeConversionInfoUnsupported(auth_component),
1676 )),
1677 }
1678}
1679fn validate_output_note_senders(
1687 transaction_request: &TransactionRequest,
1688 account_id: AccountId,
1689) -> Result<(), ClientError> {
1690 for note in transaction_request.expected_output_own_notes() {
1691 let sender = note.metadata().sender();
1692 if sender != account_id {
1693 return Err(ClientError::TransactionRequestError(
1694 TransactionRequestError::OutputNoteSenderMismatch {
1695 expected: account_id,
1696 actual: sender,
1697 },
1698 ));
1699 }
1700 }
1701
1702 Ok(())
1703}
1704
1705fn validate_basic_account_request(
1708 transaction_request: &TransactionRequest,
1709 vault_assets: &[Asset],
1710) -> Result<(), ClientError> {
1711 let (fungible_balance_map, non_fungible_set) = get_outgoing_assets(transaction_request);
1713
1714 let (incoming_fungible_balance_map, incoming_non_fungible_balance_set) =
1716 transaction_request.incoming_assets();
1717
1718 let mut available_fungible: BTreeMap<AccountId, u64> = BTreeMap::new();
1721 for asset in vault_assets {
1722 if let Asset::Fungible(fungible) = asset {
1723 let balance = available_fungible.entry(fungible.faucet_id()).or_default();
1724 *balance = balance.saturating_add(fungible.amount().as_u64());
1725 }
1726 }
1727
1728 for (faucet_id, amount) in fungible_balance_map {
1731 let account_asset_amount = available_fungible.get(&faucet_id).copied().unwrap_or(0);
1732 let incoming_balance = incoming_fungible_balance_map.get(&faucet_id).unwrap_or(&0);
1733 if account_asset_amount + incoming_balance < amount {
1734 return Err(ClientError::AssetError(AssetError::FungibleAssetAmountNotSufficient {
1735 minuend: account_asset_amount,
1736 subtrahend: amount,
1737 }));
1738 }
1739 }
1740
1741 for non_fungible in &non_fungible_set {
1744 let held = vault_assets
1745 .iter()
1746 .any(|asset| matches!(asset, Asset::NonFungible(nf) if nf == 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
1868#[cfg(test)]
1872mod tests {
1873 use alloc::vec;
1874
1875 use miden_protocol::Word;
1876 use miden_protocol::account::auth::AuthSecretKey;
1877 use miden_protocol::account::{
1878 Account,
1879 AccountBuilder,
1880 AccountComponent,
1881 AccountComponentMetadata,
1882 AccountId,
1883 AccountType,
1884 };
1885 use miden_protocol::asset::FungibleAsset;
1886 use miden_protocol::block::{BlockHeader, BlockNumber, FeeParameters};
1887 use miden_protocol::crypto::rand::RandomCoin;
1888 use miden_protocol::note::{Note, NoteType};
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_protocol::testing::validator_keys::random_validator_set;
1896 use miden_standards::account::AccountBuilderSchemaCommitmentExt;
1897 use miden_standards::account::auth::{
1898 Approver,
1899 ApproverSet,
1900 AuthGuardedMultisig,
1901 AuthGuardedMultisigConfig,
1902 AuthMultisig,
1903 AuthMultisigConfig,
1904 AuthMultisigSmart,
1905 AuthMultisigSmartConfig,
1906 AuthSingleSig,
1907 FeeConversionInfo,
1908 GuardianConfig,
1909 NoAuth,
1910 commit_fee_conversion_info,
1911 };
1912 use miden_standards::account::wallets::BasicWallet;
1913 use miden_standards::note::P2idNote;
1914
1915 use super::{
1916 AccountComponentInterface,
1917 NATIVE_FEE_CONVERSION_SALT,
1918 TransactionRequest,
1919 TransactionRequestBuilder,
1920 attach_native_fee_conversion_info,
1921 validate_fee_conversion_info_support,
1922 validate_output_note_senders,
1923 };
1924 use crate::ClientError;
1925 use crate::assembly::CodeBuilder;
1926 use crate::auth::AuthSchemeId;
1927 use crate::transaction::TransactionRequestError;
1928
1929 fn own_note_with_sender(sender: AccountId) -> Note {
1930 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
1931 let target_id =
1932 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1933 let mut rng = RandomCoin::new(Word::default());
1934
1935 P2idNote::builder()
1936 .sender(sender)
1937 .target(target_id)
1938 .asset(FungibleAsset::new(faucet_id, 100).unwrap())
1939 .note_type(NoteType::Public)
1940 .generate_serial_number(&mut rng)
1941 .build()
1942 .expect("note creation failed")
1943 .into()
1944 }
1945
1946 #[test]
1947 fn output_note_with_foreign_sender_is_rejected() {
1948 let account_id =
1949 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1950 let foreign_sender = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
1951 assert_ne!(account_id, foreign_sender);
1952
1953 let request = TransactionRequestBuilder::new()
1954 .own_output_notes(vec![own_note_with_sender(foreign_sender)])
1955 .build()
1956 .unwrap();
1957
1958 let err = validate_output_note_senders(&request, account_id).unwrap_err();
1959 match err {
1960 ClientError::TransactionRequestError(
1961 TransactionRequestError::OutputNoteSenderMismatch { expected, actual },
1962 ) => {
1963 assert_eq!(expected, account_id);
1964 assert_eq!(actual, foreign_sender);
1965 },
1966 other => panic!("expected OutputNoteSenderMismatch, got {other:?}"),
1967 }
1968 }
1969
1970 #[test]
1971 fn output_note_with_matching_sender_is_accepted() {
1972 let account_id =
1973 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1974
1975 let request = TransactionRequestBuilder::new()
1976 .own_output_notes(vec![own_note_with_sender(account_id)])
1977 .build()
1978 .unwrap();
1979
1980 validate_output_note_senders(&request, account_id).unwrap();
1981 }
1982
1983 #[test]
1984 fn request_without_own_output_notes_is_accepted() {
1985 let account_id =
1986 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1987 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
1988
1989 let request = TransactionRequestBuilder::new()
1991 .input_notes(vec![(own_note_with_sender(faucet_id), None)])
1992 .build()
1993 .unwrap();
1994
1995 validate_output_note_senders(&request, account_id).unwrap();
1996 }
1997
1998 fn account_with_auth(auth_component: impl Into<AccountComponent>) -> Account {
2000 AccountBuilder::new([7u8; 32])
2001 .account_type(AccountType::Public)
2002 .with_component(auth_component)
2003 .with_component(BasicWallet)
2004 .build_with_schema_commitment()
2005 .expect("account creation failed")
2006 }
2007
2008 fn fee_conversion_request() -> TransactionRequest {
2009 TransactionRequestBuilder::new()
2010 .fee_conversion_salt(Word::from([13u32, 14, 15, 16]))
2011 .build()
2012 .unwrap()
2013 }
2014
2015 #[test]
2016 fn fee_conversion_info_is_accepted_by_a_signature_authenticated_account() {
2017 let key = AuthSecretKey::new_falcon512_poseidon2();
2018 let auth = AuthSingleSig::new(Approver::new(
2019 key.public_key().to_commitment(),
2020 AuthSchemeId::Falcon512Poseidon2,
2021 ));
2022
2023 validate_fee_conversion_info_support(
2024 &fee_conversion_request(),
2025 &account_with_auth(auth).code_interface(),
2026 )
2027 .unwrap();
2028 }
2029
2030 #[test]
2031 fn fee_conversion_info_is_rejected_by_an_account_that_cannot_read_it() {
2032 let account = account_with_auth(NoAuth);
2033
2034 let err = validate_fee_conversion_info_support(
2035 &fee_conversion_request(),
2036 &account.code_interface(),
2037 )
2038 .expect_err("NoAuth does not read the auth args");
2039 match err {
2040 ClientError::TransactionRequestError(
2041 TransactionRequestError::FeeConversionInfoUnsupported(auth_component),
2042 ) => assert_eq!(auth_component, AccountComponentInterface::AuthNoAuth.name()),
2043 other => panic!("expected FeeConversionInfoUnsupported, got {other:?}"),
2044 }
2045 }
2046
2047 #[test]
2048 fn a_request_without_fee_conversion_info_skips_the_auth_component_check() {
2049 validate_fee_conversion_info_support(
2051 &TransactionRequestBuilder::new().build().unwrap(),
2052 &account_with_auth(NoAuth).code_interface(),
2053 )
2054 .unwrap();
2055 }
2056
2057 const NATIVE_FEE_FAUCET: u128 = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET;
2063
2064 fn header_with_base_fee(verification_base_fee: u32) -> BlockHeader {
2067 let fee_parameters = FeeParameters::new(
2068 AccountId::try_from(NATIVE_FEE_FAUCET).unwrap(),
2069 verification_base_fee,
2070 );
2071 let (_, validator_keys) = random_validator_set(1);
2072
2073 BlockHeader::new(
2074 1,
2075 Word::empty(),
2076 BlockNumber::from(1u32),
2077 Word::empty(),
2078 Word::empty(),
2079 Word::empty(),
2080 Word::empty(),
2081 Word::empty(),
2082 Word::empty(),
2083 validator_keys,
2084 fee_parameters,
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 );
2101 *request.auth_arg()
2102 }
2103
2104 fn try_injected_auth_arg(
2106 mut request: TransactionRequest,
2107 account: &Account,
2108 verification_base_fee: u32,
2109 ) -> Result<Option<Word>, ClientError> {
2110 attach_native_fee_conversion_info(
2111 &mut request,
2112 &account.code_interface(),
2113 &header_with_base_fee(verification_base_fee),
2114 )?;
2115 Ok(*request.auth_arg())
2116 }
2117
2118 fn singlesig_account() -> Account {
2119 let key = AuthSecretKey::new_falcon512_poseidon2();
2120 account_with_auth(AuthSingleSig::new(Approver::new(
2121 key.public_key().to_commitment(),
2122 AuthSchemeId::Falcon512Poseidon2,
2123 )))
2124 }
2125
2126 fn guarded_multisig_account() -> Account {
2127 let approvers = ApproverSet::new(
2128 vec![Approver::new(
2129 AuthSecretKey::new_falcon512_poseidon2().public_key().to_commitment(),
2130 AuthSchemeId::Falcon512Poseidon2,
2131 )],
2132 1,
2133 )
2134 .unwrap();
2135
2136 let guardian = GuardianConfig::new(Approver::new(
2137 AuthSecretKey::new_falcon512_poseidon2().public_key().to_commitment(),
2138 AuthSchemeId::Falcon512Poseidon2,
2139 ));
2140
2141 account_with_auth(
2142 AuthGuardedMultisig::new(AuthGuardedMultisigConfig::new(approvers, guardian).unwrap())
2143 .unwrap(),
2144 )
2145 }
2146
2147 #[test]
2148 fn native_fee_conversion_info_is_attached_on_a_fee_charging_chain() {
2149 let auth_arg = injected_auth_arg(
2150 TransactionRequestBuilder::new().build().unwrap(),
2151 &singlesig_account(),
2152 500,
2153 )
2154 .expect("a fee-charging chain should get conversion info attached");
2155
2156 let (expected, _) = commit_fee_conversion_info(
2157 FeeConversionInfo::one_to_one(AccountId::try_from(NATIVE_FEE_FAUCET).unwrap()),
2158 NATIVE_FEE_CONVERSION_SALT,
2159 );
2160 assert_eq!(auth_arg, expected, "the fee should be paid in the native asset at rate 1/1");
2161 }
2162
2163 #[test]
2164 fn an_explicit_auth_arg_is_not_overwritten() {
2165 let auth_arg = Word::from([21u32, 22, 23, 24]);
2166 let request = TransactionRequestBuilder::new().auth_arg(auth_arg).build().unwrap();
2167
2168 assert_eq!(
2169 injected_auth_arg(request, &singlesig_account(), 500),
2170 Some(auth_arg),
2171 "a request that declares its own auth arg keeps it"
2172 );
2173 }
2174
2175 fn native_commitment(salt: Word) -> Word {
2177 let (auth_arg, _) = commit_fee_conversion_info(
2178 FeeConversionInfo::one_to_one(AccountId::try_from(NATIVE_FEE_FAUCET).unwrap()),
2179 salt,
2180 );
2181 auth_arg
2182 }
2183
2184 #[test]
2185 fn a_declared_salt_is_used_for_the_native_commitment() {
2186 let salt = Word::from([17u32, 18, 19, 20]);
2187 let request = TransactionRequestBuilder::new().fee_conversion_salt(salt).build().unwrap();
2188
2189 let auth_arg = injected_auth_arg(request, &singlesig_account(), 500)
2190 .expect("a declared salt should still get native conversion info attached");
2191
2192 assert_eq!(auth_arg, native_commitment(salt));
2193 }
2194
2195 fn multisig_account() -> Account {
2196 let approvers = ApproverSet::new(
2197 vec![Approver::new(
2198 AuthSecretKey::new_falcon512_poseidon2().public_key().to_commitment(),
2199 AuthSchemeId::Falcon512Poseidon2,
2200 )],
2201 1,
2202 )
2203 .unwrap();
2204
2205 account_with_auth(AuthMultisig::new(AuthMultisigConfig::new(approvers)).unwrap())
2206 }
2207
2208 #[test]
2209 fn nothing_is_attached_where_it_is_not_needed_or_not_readable() {
2210 for (case, account, base_fee) in [
2211 ("a zero base fee charges nothing", singlesig_account(), 0),
2212 ("NoAuth never reads the auth args", account_with_auth(NoAuth), 500),
2213 ("a multisig salt is its own replay guard", multisig_account(), 500),
2214 ] {
2215 assert_eq!(
2216 injected_auth_arg(
2217 TransactionRequestBuilder::new().build().unwrap(),
2218 &account,
2219 base_fee
2220 ),
2221 None,
2222 "{case}"
2223 );
2224 }
2225 }
2226
2227 #[test]
2234 fn fee_conversion_info_is_accepted_by_a_guarded_multisig_account() {
2235 validate_fee_conversion_info_support(
2236 &fee_conversion_request(),
2237 &guarded_multisig_account().code_interface(),
2238 )
2239 .expect("a guarded multisig reads the auth args as conversion info");
2240 }
2241
2242 #[test]
2246 fn a_guarded_multisig_account_must_declare_its_own_fee_conversion_info() {
2247 let err = try_injected_auth_arg(
2248 TransactionRequestBuilder::new().build().unwrap(),
2249 &guarded_multisig_account(),
2250 500,
2251 )
2252 .expect_err("a guarded multisig account cannot inherit the fixed native salt");
2253 match err {
2254 ClientError::TransactionRequestError(
2255 TransactionRequestError::FeeConversionInfoRequired(auth_component),
2256 ) => {
2257 assert_eq!(auth_component, AccountComponentInterface::AuthGuardedMultisig.name());
2258 },
2259 other => panic!("expected FeeConversionInfoRequired, got {other:?}"),
2260 }
2261
2262 let salt = Word::from([13u32, 14, 15, 16]);
2263 assert_eq!(
2264 try_injected_auth_arg(fee_conversion_request(), &guarded_multisig_account(), 500)
2265 .expect("a declared salt is accepted"),
2266 Some(native_commitment(salt)),
2267 "a guarded multisig account that declares a salt commits the native conversion info"
2268 );
2269
2270 assert_eq!(
2271 try_injected_auth_arg(
2272 TransactionRequestBuilder::new().build().unwrap(),
2273 &guarded_multisig_account(),
2274 0
2275 )
2276 .expect("a chain charging nothing needs no conversion info"),
2277 None,
2278 );
2279 }
2280
2281 fn smart_multisig_account() -> Account {
2285 let approvers = ApproverSet::new(
2286 vec![Approver::new(
2287 AuthSecretKey::new_falcon512_poseidon2().public_key().to_commitment(),
2288 AuthSchemeId::Falcon512Poseidon2,
2289 )],
2290 1,
2291 )
2292 .unwrap();
2293
2294 account_with_auth(AuthMultisigSmart::new(AuthMultisigSmartConfig::new(approvers)).unwrap())
2295 }
2296
2297 #[test]
2302 fn fee_conversion_info_is_accepted_by_a_smart_multisig_account() {
2303 validate_fee_conversion_info_support(
2304 &fee_conversion_request(),
2305 &smart_multisig_account().code_interface(),
2306 )
2307 .expect("a smart multisig reads the auth args as conversion info");
2308 }
2309
2310 #[test]
2314 fn a_smart_multisig_account_must_declare_its_own_fee_conversion_info() {
2315 let err = try_injected_auth_arg(
2316 TransactionRequestBuilder::new().build().unwrap(),
2317 &smart_multisig_account(),
2318 500,
2319 )
2320 .expect_err("a smart multisig account cannot inherit the fixed native salt");
2321 match err {
2322 ClientError::TransactionRequestError(
2323 TransactionRequestError::FeeConversionInfoRequired(auth_component),
2324 ) => {
2325 assert_eq!(auth_component, AccountComponentInterface::AuthMultisigSmart.name());
2326 },
2327 other => panic!("expected FeeConversionInfoRequired, got {other:?}"),
2328 }
2329
2330 let salt = Word::from([13u32, 14, 15, 16]);
2331 assert_eq!(
2332 try_injected_auth_arg(fee_conversion_request(), &smart_multisig_account(), 500)
2333 .expect("a declared salt is accepted"),
2334 Some(native_commitment(salt)),
2335 "a smart multisig account that declares a salt commits the native conversion info"
2336 );
2337
2338 assert_eq!(
2339 try_injected_auth_arg(
2340 TransactionRequestBuilder::new().build().unwrap(),
2341 &smart_multisig_account(),
2342 0
2343 )
2344 .expect("a chain charging nothing needs no conversion info"),
2345 None,
2346 );
2347 }
2348
2349 #[test]
2352 fn an_unrecognized_auth_component_is_rejected_rather_than_panicking() {
2353 const CUSTOM_AUTH: &str = "
2354 use miden::protocol::native_account
2355
2356 @auth_script
2357 pub proc auth_custom
2358 exec.native_account::incr_nonce
2359 drop
2360 end
2361 ";
2362
2363 let code = CodeBuilder::default()
2364 .compile_component_code("miden::testing::custom_auth", CUSTOM_AUTH)
2365 .expect("custom auth component code should compile");
2366 let auth = AccountComponent::new(
2367 code,
2368 vec![],
2369 AccountComponentMetadata::new("miden::testing::custom_auth"),
2370 )
2371 .expect("custom auth component");
2372
2373 let account = account_with_auth(auth);
2374
2375 let err = validate_fee_conversion_info_support(
2376 &fee_conversion_request(),
2377 &account.code_interface(),
2378 )
2379 .expect_err("an account with no recognized auth component cannot read conversion info");
2380 assert!(matches!(
2381 err,
2382 ClientError::TransactionRequestError(
2383 TransactionRequestError::FeeConversionInfoUnsupported(_)
2384 )
2385 ));
2386
2387 assert_eq!(
2388 try_injected_auth_arg(TransactionRequestBuilder::new().build().unwrap(), &account, 500)
2389 .expect("a request declaring nothing is left alone"),
2390 None,
2391 "an auth component nothing can reason about gets nothing attached"
2392 );
2393 }
2394}