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(
374 &mut self,
375 account_id: AccountId,
376 transaction_request: TransactionRequest,
377 anchor: ChainAnchor,
378 ) -> Result<TransactionResult, ClientError> {
379 let result = self
380 .execute_transaction_with_mode(
381 account_id,
382 transaction_request,
383 TransactionExecutionMode::Standard,
384 Some(Box::new(anchor)),
385 )
386 .await?;
387
388 let expiration = result.executed_transaction().expiration_block_num();
393 let sync_height = self.store.get_sync_height().await?;
394 if expiration <= sync_height {
395 return Err(
396 ChainAnchorError::AnchoredTransactionExpired { expiration, sync_height }.into()
397 );
398 }
399
400 Ok(result)
401 }
402
403 async fn chain_anchor_at_tip(
408 &self,
409 tracked_blocks: BTreeSet<BlockNumber>,
410 ) -> Result<ChainAnchor, ClientError> {
411 let sync_height = self.store.get_sync_height().await?;
412
413 let (header, _had_notes) = self
414 .store
415 .get_block_header_by_num(sync_height)
416 .await?
417 .ok_or(StoreError::BlockHeaderNotFound(sync_height))?;
418
419 let mut tracked_blocks = tracked_blocks;
420 tracked_blocks.remove(&sync_height);
422
423 let block_headers: Vec<BlockHeader> = self
424 .store
425 .get_block_headers(&tracked_blocks)
426 .await?
427 .into_iter()
428 .map(|(header, _has_notes)| header)
429 .collect();
430
431 let fetched_nums: BTreeSet<BlockNumber> =
434 block_headers.iter().map(BlockHeader::block_num).collect();
435 if let Some(&missing) = tracked_blocks.difference(&fetched_nums).next() {
436 return Err(StoreError::BlockHeaderNotFound(missing).into());
437 }
438
439 let peaks = self.store.get_current_blockchain_peaks().await?;
440 let partial_mmr = build_partial_mmr_with_paths(&self.store, peaks, &block_headers).await?;
441
442 let chain = PartialBlockchain::new(partial_mmr, block_headers)?;
443
444 Ok(ChainAnchor::new(header, chain)?)
445 }
446
447 pub async fn chain_anchor_for_request(
465 &self,
466 transaction_request: &TransactionRequest,
467 ) -> Result<ChainAnchor, ClientError> {
468 let input_note_ids: Vec<NoteId> = transaction_request.input_note_ids().collect();
469
470 let tracked_blocks: BTreeSet<BlockNumber> = if input_note_ids.is_empty() {
471 BTreeSet::new()
472 } else {
473 self.store
474 .get_input_notes(NoteFilter::List(input_note_ids))
475 .await?
476 .iter()
477 .filter(|record| record.is_authenticated())
478 .filter_map(|record| record.inclusion_proof())
479 .map(|proof| proof.location().block_num())
480 .collect()
481 };
482
483 self.chain_anchor_at_tip(tracked_blocks).await
484 }
485
486 #[cfg(feature = "dap")]
500 pub async fn execute_transaction_with_dap(
501 &self,
502 account_id: AccountId,
503 transaction_request: TransactionRequest,
504 ) -> Result<TransactionResult, ClientError> {
505 self.execute_transaction_with_mode(
506 account_id,
507 transaction_request,
508 TransactionExecutionMode::Dap,
509 None,
510 )
511 .await
512 }
513
514 async fn execute_transaction_with_mode(
518 &self,
519 account_id: AccountId,
520 transaction_request: TransactionRequest,
521 execution_mode: TransactionExecutionMode,
522 anchor: Option<Box<ChainAnchor>>,
523 ) -> Result<TransactionResult, ClientError> {
524 let account: PartialAccount =
525 self.get_native_account_record(account_id).await?.try_into()?;
526
527 let prep = self
528 .prepare_transaction(&account, transaction_request, anchor.as_deref())
529 .await?;
530
531 let mut data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
532 if let Some(anchor) = anchor {
533 data_store = data_store.with_chain_anchor(*anchor);
534 }
535 data_store.register_note_scripts(prep.output_note_scripts());
536 for fpi_account in &prep.foreign_account_inputs {
537 data_store.mast_store().load_account_code(fpi_account.code());
538 }
539 data_store.register_foreign_account_inputs(prep.foreign_account_inputs);
540
541 data_store.mast_store().load_account_code(account.code());
542
543 let mut notes = prep.notes;
544 if prep.ignore_invalid_notes {
545 notes = self
546 .get_valid_input_notes(
547 &data_store,
548 account.id(),
549 prep.block_num,
550 notes,
551 prep.tx_args.clone(),
552 )
553 .await?;
554 }
555
556 let executed_transaction = match execution_mode {
557 TransactionExecutionMode::Standard => {
558 self.build_executor(&data_store)?
559 .execute_transaction(account_id, prep.block_num, notes, prep.tx_args)
560 .await?
561 },
562 #[cfg(feature = "dap")]
563 TransactionExecutionMode::Dap => {
564 self.build_dap_executor(&data_store)?
565 .execute_transaction(account_id, prep.block_num, notes, prep.tx_args)
566 .await?
567 },
568 };
569
570 validate_executed_transaction(&executed_transaction, &prep.output_recipients)?;
571 TransactionResult::new(executed_transaction, prep.future_notes)
572 }
573
574 pub(crate) async fn prepare_transaction(
590 &self,
591 account: &PartialAccount,
592 transaction_request: TransactionRequest,
593 anchor: Option<&ChainAnchor>,
594 ) -> Result<PreparedTransaction, ClientError> {
595 self.validate_account_request(
596 &transaction_request,
597 account.id(),
598 &account.code_interface(),
599 )
600 .await?;
601
602 self.prepare_transaction_inner(account.code_interface(), transaction_request, anchor)
603 .await
604 }
605
606 pub(crate) async fn prepare_transaction_for_batch(
607 &self,
608 account: &PartialAccount,
609 transaction_request: TransactionRequest,
610 ) -> Result<PreparedTransaction, ClientError> {
611 self.prepare_transaction_inner(account.code_interface(), transaction_request, None)
612 .await
613 }
614
615 async fn prepare_transaction_inner(
616 &self,
617 account_code_interface: AccountCodeInterface,
618 mut transaction_request: TransactionRequest,
619 anchor: Option<&ChainAnchor>,
620 ) -> Result<PreparedTransaction, ClientError> {
621 if anchor.is_none() {
622 self.validate_recency().await?;
623 }
624
625 let mut stored_note_records = self
627 .store
628 .get_input_notes(NoteFilter::List(transaction_request.input_note_ids().collect()))
629 .await?;
630
631 for note in &stored_note_records {
633 if note.is_consumed() {
634 let id = note.id().expect(
635 "stored note records reaching this check carry metadata so id() is Some",
636 );
637 return Err(ClientError::TransactionRequestError(
638 TransactionRequestError::InputNoteAlreadyConsumed(id),
639 ));
640 }
641 }
642
643 stored_note_records.retain(InputNoteRecord::is_authenticated);
645
646 let notes = transaction_request.build_input_notes(stored_note_records)?;
647
648 if let Some(anchor) = anchor {
652 for note in notes.iter() {
653 if let Some(location) = note.location() {
654 let block_num = location.block_num();
655 if block_num < anchor.block_num()
656 && !anchor.partial_blockchain().contains_block(block_num)
657 {
658 return Err(ChainAnchorError::BlockNotTracked { block_num }.into());
659 }
660 }
661 }
662 }
663
664 let output_recipients =
665 transaction_request.expected_output_recipients().cloned().collect::<Vec<_>>();
666
667 let future_notes: Vec<(NoteDetails, NoteTag)> =
668 transaction_request.expected_future_notes().cloned().collect();
669
670 let tx_script = transaction_request.build_transaction_script(&account_code_interface)?;
671
672 let foreign_accounts = transaction_request.foreign_accounts().clone();
673
674 let block_num = match anchor {
677 Some(anchor) => anchor.block_num(),
678 None => self.store.get_sync_height().await?,
679 };
680
681 let foreign_account_inputs =
682 self.retrieve_foreign_account_inputs(foreign_accounts, block_num).await?;
683
684 let ignore_invalid_notes = transaction_request.ignore_invalid_input_notes();
685
686 let reference_header = match anchor {
687 Some(anchor) => anchor.header().clone(),
688 None => {
689 self.store
690 .get_block_header_by_num(block_num)
691 .await?
692 .ok_or(StoreError::BlockHeaderNotFound(block_num))?
693 .0
694 },
695 };
696 attach_native_fee_conversion_info(
697 &mut transaction_request,
698 &account_code_interface,
699 &reference_header,
700 )?;
701
702 let tx_args = transaction_request.into_transaction_args(tx_script);
703
704 Ok(PreparedTransaction {
705 notes,
706 output_recipients,
707 future_notes,
708 tx_args,
709 foreign_account_inputs,
710 block_num,
711 ignore_invalid_notes,
712 })
713 }
714
715 pub async fn prove_transaction(
717 &self,
718 tx_result: &TransactionResult,
719 ) -> Result<ProvenTransaction, ClientError> {
720 self.prove_transaction_with(tx_result, self.tx_prover.clone()).await
721 }
722
723 pub async fn prove_transaction_with(
731 &self,
732 tx_result: &TransactionResult,
733 tx_prover: Arc<dyn TransactionProver>,
734 ) -> Result<ProvenTransaction, ClientError> {
735 info!("Proving transaction...");
736
737 let executed_transaction = tx_result.executed_transaction();
738 let proven_transaction = tx_prover.prove(executed_transaction.clone().into()).await?;
739
740 if proven_transaction.id() != executed_transaction.id() {
749 return Err(ClientError::MismatchedProvenTransaction {
750 requested: executed_transaction.id(),
751 returned: proven_transaction.id(),
752 });
753 }
754
755 info!("Transaction proven.");
756
757 Ok(proven_transaction)
758 }
759
760 pub async fn submit_proven_transaction(
763 &mut self,
764 proven_transaction: ProvenTransaction,
765 transaction_inputs: impl Into<TransactionInputs>,
766 ) -> Result<BlockNumber, ClientError> {
767 info!("Submitting transaction to the network...");
768 let tx_id = proven_transaction.id();
769 let key = self.transaction_encryption_key().await?;
770 let sealed_inputs =
771 seal_transaction_inputs(&mut self.rng, &key, tx_id, &transaction_inputs.into())?;
772 let result =
773 self.rpc_api.submit_proven_transaction(proven_transaction, sealed_inputs).await;
774 if let Err(err) = &result {
775 self.forget_stale_transaction_encryption_key(err).await;
776 }
777 let block_num = result?;
778 info!("Transaction submitted.");
779
780 Ok(block_num)
781 }
782
783 pub(crate) async fn transaction_encryption_key(
791 &self,
792 ) -> Result<TransactionEncryptionKey, ClientError> {
793 if let Some(key) = self.store.get_transaction_encryption_key().await? {
794 return Ok(key);
795 }
796
797 let attested = self.rpc_api.get_transaction_encryption_key().await?;
798
799 let genesis_commitment =
803 self.trusted_block_header(BlockNumber::GENESIS).await?.commitment();
804 let chain_tip = self.store.get_sync_height().await?;
805 let validator_keys = self.trusted_block_header(chain_tip).await?.validator_keys().clone();
806
807 let key = attested.verify(genesis_commitment, &validator_keys)?;
808 self.store.set_transaction_encryption_key(&key).await?;
809
810 Ok(key)
811 }
812
813 #[cfg(feature = "testing")]
816 pub async fn seed_transaction_encryption_key(
817 &self,
818 key: TransactionEncryptionKey,
819 ) -> Result<(), ClientError> {
820 Ok(self.store.set_transaction_encryption_key(&key).await?)
821 }
822
823 pub(crate) async fn forget_stale_transaction_encryption_key(&self, err: &RpcError) {
829 if err.is_stale_transaction_encryption_key()
830 && let Err(err) = self.store.remove_transaction_encryption_key().await
831 {
832 tracing::warn!("failed to evict the stale transaction encryption key: {err}");
833 }
834 }
835
836 async fn trusted_block_header(
843 &self,
844 block_num: BlockNumber,
845 ) -> Result<BlockHeader, ClientError> {
846 self.store.get_block_header_by_num(block_num).await?.map(|(header, _)| header).ok_or_else(
847 || {
848 ClientError::ChainValidationError(alloc::format!(
849 "block header {block_num} is not tracked locally; sync the client before it can verify data against the chain"
850 ))
851 },
852 )
853 }
854
855 pub async fn get_transaction_store_update(
858 &self,
859 tx_result: &TransactionResult,
860 submission_height: BlockNumber,
861 ) -> Result<TransactionStoreUpdate, TransactionStoreUpdateError> {
862 let note_updates = self.get_note_updates(submission_height, tx_result).await?;
863
864 let new_tags: Vec<NoteTagRecord> = note_updates
867 .updated_input_notes()
868 .filter_map(|note| {
869 let note = note.inner();
870
871 if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) =
872 note.state()
873 {
874 Some(NoteTagRecord::with_note_source(*tag, note.details_commitment()))
875 } else {
876 None
877 }
878 })
879 .collect();
880
881 Ok(TransactionStoreUpdate::new(
882 tx_result.executed_transaction().clone(),
883 submission_height,
884 note_updates,
885 tx_result.future_notes().to_vec(),
886 new_tags,
887 ))
888 }
889
890 pub async fn apply_transaction(
893 &self,
894 tx_result: &TransactionResult,
895 submission_height: BlockNumber,
896 ) -> Result<(), ClientError> {
897 let tx_update = self.get_transaction_store_update(tx_result, submission_height).await?;
898
899 self.apply_transaction_update(tx_update).await?;
900
901 for observer in &self.transaction_observers {
903 if let Err(err) = observer.apply(tx_result).await {
904 tracing::warn!(
905 observer = observer.name(),
906 error = ?err,
907 "TransactionObserver::apply failed; continuing with remaining observers",
908 );
909 }
910 }
911
912 Ok(())
913 }
914
915 pub async fn apply_transaction_update(
916 &self,
917 tx_update: TransactionStoreUpdate,
918 ) -> Result<(), ClientError> {
919 info!("Applying transaction to the local store...");
922
923 let executed_transaction = tx_update.executed_transaction();
924 let account_id = executed_transaction.account_id();
925
926 if self.account_reader(account_id).status().await?.is_locked() {
927 return Err(ClientError::AccountLocked(account_id));
928 }
929
930 self.store.apply_transaction(tx_update).await?;
931 info!("Transaction stored.");
932 Ok(())
933 }
934
935 pub async fn execute_program(
940 &self,
941 account_id: AccountId,
942 tx_script: TransactionScript,
943 advice_inputs: AdviceInputs,
944 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
945 ) -> Result<[Felt; MIN_STACK_DEPTH], ClientError> {
946 let (data_store, block_ref) =
947 self.prepare_program_execution(account_id, foreign_accounts).await?;
948
949 Ok(self
950 .build_executor(&data_store)?
951 .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
952 .await?)
953 }
954
955 #[cfg(feature = "dap")]
958 pub async fn execute_program_with_dap(
959 &self,
960 account_id: AccountId,
961 tx_script: TransactionScript,
962 advice_inputs: AdviceInputs,
963 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
964 ) -> Result<[Felt; MIN_STACK_DEPTH], ClientError> {
965 let (data_store, block_ref) =
966 self.prepare_program_execution(account_id, foreign_accounts).await?;
967
968 Ok(self
969 .build_dap_executor(&data_store)?
970 .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
971 .await?)
972 }
973
974 pub async fn validate_request(
984 &self,
985 account_id: AccountId,
986 transaction_request: &TransactionRequest,
987 ) -> Result<(), ClientError> {
988 self.validate_recency().await?;
989 validate_output_note_senders(transaction_request, account_id)?;
990 let account: PartialAccount = self
991 .store
992 .get_minimal_partial_account(account_id)
993 .await?
994 .ok_or(ClientError::AccountDataNotFound(account_id))?
995 .try_into()?;
996 self.validate_account_request(transaction_request, account_id, &account.code_interface())
997 .await
998 }
999
1000 async fn validate_account_request(
1005 &self,
1006 transaction_request: &TransactionRequest,
1007 account_id: AccountId,
1008 account_code_interface: &AccountCodeInterface,
1009 ) -> Result<(), ClientError> {
1010 validate_fee_conversion_info_support(transaction_request, account_code_interface)?;
1011
1012 if account_code_interface.contains([FungibleFaucet::mint_and_send_root()]) {
1013 Ok(())
1015 } else {
1016 let assets = self.account_reader(account_id).assets().await?;
1017 validate_basic_account_request(transaction_request, &assets)
1018 }
1019 }
1020
1021 async fn validate_recency(&self) -> Result<(), ClientError> {
1022 if let Some(max_block_number_delta) = self.max_block_number_delta {
1023 let current_chain_tip =
1024 self.rpc_api.get_block_header_by_number(None, false).await?.0.block_num();
1025
1026 if current_chain_tip > self.store.get_sync_height().await? + max_block_number_delta {
1027 return Err(ClientError::RecencyConditionError(
1028 "The client is too far behind the chain tip to execute the transaction",
1029 ));
1030 }
1031 }
1032 Ok(())
1033 }
1034
1035 pub async fn ensure_ntx_scripts_registered(
1048 &mut self,
1049 account_id: AccountId,
1050 scripts: &[NoteScript],
1051 tx_prover: Arc<dyn TransactionProver>,
1052 ) -> Result<(), ClientError> {
1053 let mut missing_scripts = Vec::new();
1054
1055 for script in scripts {
1056 if StandardNote::from_script(script).is_some() {
1058 continue;
1059 }
1060
1061 let script_root = script.root();
1062
1063 match self.rpc_api.get_note_script_by_root(script_root.into()).await {
1065 Ok(Some(_)) => {},
1066 Ok(None) => missing_scripts.push(script.clone()),
1067 Err(source) => {
1068 return Err(ClientError::NtxScriptRegistrationFailed {
1069 script_root: script_root.into(),
1070 source,
1071 });
1072 },
1073 }
1074 }
1075
1076 if missing_scripts.is_empty() {
1077 return Ok(());
1078 }
1079
1080 let registration_request = TransactionRequestBuilder::new().build_register_note_scripts(
1081 account_id,
1082 missing_scripts,
1083 self.rng(),
1084 )?;
1085
1086 let tx_result = self.execute_transaction(account_id, registration_request).await?;
1087 let proven = self.prove_transaction_with(&tx_result, tx_prover).await?;
1088 let submission_height = self.submit_proven_transaction(proven, &tx_result).await?;
1089 self.apply_transaction(&tx_result, submission_height).await?;
1090
1091 Ok(())
1092 }
1093
1094 pub(crate) async fn get_valid_input_notes<STORE: DataStore + Sync>(
1103 &self,
1104 data_store: &STORE,
1105 account_id: AccountId,
1106 block_ref: BlockNumber,
1107 mut input_notes: InputNotes<InputNote>,
1108 tx_args: TransactionArgs,
1109 ) -> Result<InputNotes<InputNote>, ClientError> {
1110 loop {
1111 if input_notes.is_empty() {
1114 break;
1115 }
1116
1117 let execution = NoteConsumptionChecker::new(&self.build_executor(data_store)?)
1118 .check_notes_consumability(
1119 account_id,
1120 block_ref,
1121 input_notes.iter().map(|n| n.clone().into_note()).collect(),
1122 tx_args.clone(),
1123 )
1124 .await?;
1125
1126 if execution.failed().is_empty() {
1127 break;
1128 }
1129
1130 let failed_note_ids: BTreeSet<NoteId> =
1131 execution.failed().iter().map(|n| n.note().id()).collect();
1132 let filtered_input_notes = InputNotes::new(
1133 input_notes
1134 .into_iter()
1135 .filter(|note| !failed_note_ids.contains(¬e.id()))
1136 .collect(),
1137 )
1138 .expect("Created from a valid input notes list");
1139
1140 input_notes = filtered_input_notes;
1141 }
1142
1143 Ok(input_notes)
1144 }
1145
1146 async fn retrieve_foreign_account_inputs(
1155 &self,
1156 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
1157 block_num: BlockNumber,
1158 ) -> Result<Vec<AccountInputs>, ClientError> {
1159 if foreign_accounts.is_empty() {
1160 return Ok(Vec::new());
1161 }
1162
1163 let mut return_foreign_account_inputs = Vec::with_capacity(foreign_accounts.len());
1164
1165 for foreign_account in foreign_accounts.into_values() {
1166 let foreign_account_inputs = match foreign_account {
1167 ForeignAccount::Public(account_id, storage_requirements) => {
1168 fetch_public_account_inputs(
1169 &self.store,
1170 &self.rpc_api,
1171 account_id,
1172 storage_requirements,
1173 AccountStateAt::Block(block_num),
1174 )
1175 .await?
1176 },
1177 ForeignAccount::Private(partial_account) => {
1178 let account_id = partial_account.id();
1179 let (_, account_proof) = self
1180 .rpc_api
1181 .get_account(
1182 account_id,
1183 GetAccountRequest::new().at(AccountStateAt::Block(block_num)),
1184 )
1185 .await?;
1186 let (witness, _) = account_proof.into_parts();
1187 AccountInputs::new(partial_account, witness)
1188 },
1189 };
1190
1191 return_foreign_account_inputs.push(foreign_account_inputs);
1192 }
1193
1194 Ok(return_foreign_account_inputs)
1195 }
1196
1197 async fn prepare_program_execution(
1201 &self,
1202 account_id: AccountId,
1203 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
1204 ) -> Result<(ClientDataStore, BlockNumber), ClientError> {
1205 let block_ref = self.get_sync_height().await?;
1206
1207 let foreign_account_inputs =
1208 self.retrieve_foreign_account_inputs(foreign_accounts, block_ref).await?;
1209
1210 let account_code = self
1211 .store
1212 .get_account_code(account_id)
1213 .await?
1214 .ok_or(ClientError::AccountDataNotFound(account_id))?;
1215
1216 let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
1217
1218 data_store.mast_store().load_account_code(&account_code);
1220
1221 for fpi_account in &foreign_account_inputs {
1222 data_store.mast_store().load_account_code(fpi_account.code());
1223 }
1224
1225 data_store.register_foreign_account_inputs(foreign_account_inputs);
1226
1227 Ok((data_store, block_ref))
1228 }
1229
1230 pub(crate) fn build_executor<'store, 'auth, STORE: DataStore + Sync>(
1233 &'auth self,
1234 data_store: &'store STORE,
1235 ) -> Result<TransactionExecutor<'store, 'auth, STORE, AUTH>, TransactionExecutorError> {
1236 let mut executor = TransactionExecutor::new(data_store)
1237 .with_options(self.exec_options)?
1238 .with_source_manager(self.source_manager.clone());
1239 if let Some(authenticator) = self.authenticator.as_deref() {
1240 executor = executor.with_authenticator(authenticator);
1241 }
1242 Ok(executor)
1243 }
1244
1245 async fn get_native_account_record(
1250 &self,
1251 account_id: AccountId,
1252 ) -> Result<AccountRecord, ClientError> {
1253 let account_record = self
1254 .store
1255 .get_minimal_partial_account(account_id)
1256 .await?
1257 .ok_or(ClientError::AccountDataNotFound(account_id))?;
1258 if account_record.is_watched() {
1259 return Err(ClientError::AccountIsWatched(account_id));
1260 }
1261 Ok(account_record)
1262 }
1263
1264 #[cfg(feature = "dap")]
1266 pub(crate) fn build_dap_executor<'store, 'auth, STORE: DataStore + Sync>(
1267 &'auth self,
1268 data_store: &'store STORE,
1269 ) -> Result<
1270 TransactionExecutor<'store, 'auth, STORE, AUTH, dap_executor::DapProgramExecutor>,
1271 TransactionExecutorError,
1272 > {
1273 Ok(self
1274 .build_executor(data_store)?
1275 .with_program_executor::<dap_executor::DapProgramExecutor>())
1276 }
1277
1278 async fn get_note_updates(
1281 &self,
1282 submission_height: BlockNumber,
1283 tx_result: &TransactionResult,
1284 ) -> Result<NoteUpdateTracker, TransactionStoreUpdateError> {
1285 let executed_tx = tx_result.executed_transaction();
1286 let current_timestamp = self.store.get_current_timestamp();
1287 let current_block_num = self.store.get_sync_height().await?;
1288
1289 let new_output_notes = executed_tx
1301 .output_notes()
1302 .iter()
1303 .filter(|output_note| {
1304 output_note
1305 .recipient()
1306 .is_none_or(|recipient| recipient.script().root() != TxFeeNote::script_root())
1307 })
1308 .cloned()
1309 .filter_map(|output_note| {
1310 OutputNoteRecord::try_from_output_note(output_note, submission_height).ok()
1311 })
1312 .collect::<Vec<_>>();
1313
1314 let mut new_input_notes = vec![];
1316 let output_notes: Vec<Note> =
1317 notes_from_output(executed_tx.output_notes()).cloned().collect();
1318 let note_screener = self.note_screener().clone();
1319 let output_note_relevances = note_screener.get_batch_consumability(&output_notes).await?;
1320
1321 for note in output_notes {
1322 if note.script().root() == TxFeeNote::script_root() {
1327 continue;
1328 }
1329
1330 if output_note_relevances.contains_key(¬e.id()) {
1331 let metadata = *note.metadata();
1332 let tag = metadata.tag();
1333 let attachments = note.attachments().clone();
1334
1335 new_input_notes.push(InputNoteRecord::new(
1336 note.into(),
1337 attachments,
1338 current_timestamp,
1339 ExpectedNoteState {
1340 metadata: Some(metadata),
1341 after_block_num: submission_height,
1342 tag: Some(tag),
1343 }
1344 .into(),
1345 ));
1346 }
1347 }
1348
1349 new_input_notes.extend(tx_result.future_notes().iter().map(|(note_details, tag)| {
1351 InputNoteRecord::new(
1352 note_details.clone(),
1353 NoteAttachments::empty(),
1354 None,
1355 ExpectedNoteState {
1356 metadata: None,
1357 after_block_num: current_block_num,
1358 tag: Some(*tag),
1359 }
1360 .into(),
1361 )
1362 }));
1363
1364 let consumed_note_ids =
1369 executed_tx.tx_inputs().input_notes().iter().map(InputNote::id).collect();
1370
1371 let consumed_notes =
1372 self.store.get_input_notes(NoteFilter::List(consumed_note_ids)).await?;
1373
1374 let tracked_note_ids =
1375 consumed_notes.iter().filter_map(InputNoteRecord::id).collect::<BTreeSet<_>>();
1376
1377 for input_note in executed_tx.tx_inputs().input_notes() {
1378 if !tracked_note_ids.contains(&input_note.id()) {
1379 let mut input_note_record = InputNoteRecord::from(input_note.clone());
1380 input_note_record.consumed_locally(
1381 executed_tx.account_id(),
1382 executed_tx.id(),
1383 current_timestamp,
1384 )?;
1385 new_input_notes.push(input_note_record);
1386 }
1387 }
1388
1389 let mut updated_input_notes = vec![];
1390
1391 for mut input_note_record in consumed_notes {
1392 if input_note_record.consumed_locally(
1393 executed_tx.account_id(),
1394 executed_tx.id(),
1395 current_timestamp,
1396 )? {
1397 updated_input_notes.push(input_note_record);
1398 }
1399 }
1400
1401 Ok(NoteUpdateTracker::for_transaction_updates(
1402 new_input_notes,
1403 updated_input_notes,
1404 new_output_notes,
1405 ))
1406 }
1407}
1408
1409#[derive(Debug, thiserror::Error)]
1415pub enum TransactionStoreUpdateError {
1416 #[error("store error")]
1417 Store(#[from] StoreError),
1418 #[error("note screener error")]
1419 NoteScreener(#[from] NoteScreenerError),
1420 #[error("note record error")]
1421 NoteRecord(#[from] NoteRecordError),
1422}
1423
1424#[derive(Clone, Copy, Debug)]
1428enum TransactionExecutionMode {
1429 Standard,
1430 #[cfg(feature = "dap")]
1431 Dap,
1432}
1433
1434pub(crate) struct PreparedTransaction {
1436 pub(crate) notes: InputNotes<InputNote>,
1437 pub(crate) output_recipients: Vec<NoteRecipient>,
1438 pub(crate) future_notes: Vec<(NoteDetails, NoteTag)>,
1439 pub(crate) tx_args: TransactionArgs,
1440 pub(crate) foreign_account_inputs: Vec<AccountInputs>,
1441 pub(crate) block_num: BlockNumber,
1442 pub(crate) ignore_invalid_notes: bool,
1443}
1444
1445impl PreparedTransaction {
1446 pub(crate) fn output_note_scripts(&self) -> impl Iterator<Item = NoteScript> + '_ {
1449 self.output_recipients.iter().map(|recipient| recipient.script().clone())
1450 }
1451}
1452
1453fn get_outgoing_assets(
1458 transaction_request: &TransactionRequest,
1459) -> (BTreeMap<AccountId, u64>, Vec<NonFungibleAsset>) {
1460 let mut own_notes_assets = match transaction_request.script_template() {
1462 Some(TransactionScriptTemplate::SendNotes(notes)) => notes
1463 .iter()
1464 .map(|note| (note.id(), note.assets().clone()))
1465 .collect::<BTreeMap<_, _>>(),
1466 _ => BTreeMap::default(),
1467 };
1468 let mut output_notes_assets = transaction_request
1470 .expected_output_own_notes()
1471 .into_iter()
1472 .map(|note| (note.id(), note.assets().clone()))
1473 .collect::<BTreeMap<_, _>>();
1474
1475 output_notes_assets.append(&mut own_notes_assets);
1477
1478 let outgoing_assets = output_notes_assets.values().flat_map(|note_assets| note_assets.iter());
1480
1481 request::collect_assets(outgoing_assets)
1482}
1483
1484fn attach_native_fee_conversion_info(
1499 transaction_request: &mut TransactionRequest,
1500 account_code_interface: &AccountCodeInterface,
1501 reference_header: &BlockHeader,
1502) -> Result<(), ClientError> {
1503 if transaction_request.has_auth_arg() {
1507 return Ok(());
1508 }
1509
1510 let fee_parameters = reference_header.fee_parameters();
1511 let declared_salt = transaction_request.fee_conversion_salt();
1512 if fee_parameters.verification_base_fee() == 0 && declared_salt.is_none() {
1513 return Ok(());
1514 }
1515
1516 match FeeAuth::of(account_code_interface) {
1517 FeeAuth::FixedSalt => {
1518 transaction_request.commit_native_fee_conversion_info(
1519 fee_parameters.fee_faucet_id(),
1520 declared_salt.unwrap_or(NATIVE_FEE_CONVERSION_SALT),
1521 );
1522 Ok(())
1523 },
1524 FeeAuth::CallerChosenSalt(component) => match declared_salt {
1525 Some(salt) => {
1526 transaction_request
1527 .commit_native_fee_conversion_info(fee_parameters.fee_faucet_id(), salt);
1528 Ok(())
1529 },
1530 None => Err(ClientError::TransactionRequestError(
1531 TransactionRequestError::FeeConversionInfoRequired(component),
1532 )),
1533 },
1534 FeeAuth::Ignored(component) => match declared_salt {
1535 Some(_) => Err(ClientError::TransactionRequestError(
1538 TransactionRequestError::FeeConversionInfoUnsupported(component),
1539 )),
1540 None => Ok(()),
1541 },
1542 }
1543}
1544
1545enum FeeAuth {
1547 FixedSalt,
1550 CallerChosenSalt(String),
1553 Ignored(String),
1557}
1558
1559impl FeeAuth {
1560 fn of(account_code_interface: &AccountCodeInterface) -> Self {
1569 let procedures: Vec<_> = account_code_interface.procedures().iter().copied().collect();
1570 let components = AccountComponentInterface::from_procedures(&procedures);
1571
1572 if components
1573 .iter()
1574 .any(|component| matches!(component, AccountComponentInterface::AuthSingleSig))
1575 {
1576 return Self::FixedSalt;
1577 }
1578
1579 let caller_chosen_salt = components.iter().find_map(|component| match component {
1584 AccountComponentInterface::AuthMultisig
1585 | AccountComponentInterface::AuthMultisigSmart
1586 | AccountComponentInterface::AuthGuardedMultisig => {
1587 Some(Self::CallerChosenSalt(component.name()))
1588 },
1589 _ => None,
1590 });
1591
1592 caller_chosen_salt.unwrap_or_else(|| {
1593 let name = components
1594 .iter()
1595 .find(|component| {
1596 matches!(
1597 component,
1598 AccountComponentInterface::AuthNoAuth
1599 | AccountComponentInterface::AuthNetworkAccount
1600 )
1601 })
1602 .map_or_else(|| "unrecognized".into(), AccountComponentInterface::name);
1603
1604 Self::Ignored(name)
1605 })
1606 }
1607}
1608
1609pub(crate) fn native_fee_conversion_info(
1614 account_code_interface: &AccountCodeInterface,
1615 fee_parameters: &FeeParameters,
1616) -> Option<FeeConversionInfo> {
1617 if fee_parameters.verification_base_fee() == 0 {
1618 return None;
1619 }
1620
1621 match FeeAuth::of(account_code_interface) {
1624 FeeAuth::FixedSalt => Some(FeeConversionInfo::one_to_one(fee_parameters.fee_faucet_id())),
1625 FeeAuth::CallerChosenSalt(_) | FeeAuth::Ignored(_) => None,
1626 }
1627}
1628
1629fn validate_fee_conversion_info_support(
1635 transaction_request: &TransactionRequest,
1636 account_code_interface: &AccountCodeInterface,
1637) -> Result<(), ClientError> {
1638 if transaction_request.fee_conversion_salt().is_none() {
1639 return Ok(());
1640 }
1641
1642 match FeeAuth::of(account_code_interface) {
1643 FeeAuth::FixedSalt | FeeAuth::CallerChosenSalt(_) => Ok(()),
1644 FeeAuth::Ignored(auth_component) => Err(ClientError::TransactionRequestError(
1645 TransactionRequestError::FeeConversionInfoUnsupported(auth_component),
1646 )),
1647 }
1648}
1649fn validate_output_note_senders(
1657 transaction_request: &TransactionRequest,
1658 account_id: AccountId,
1659) -> Result<(), ClientError> {
1660 for note in transaction_request.expected_output_own_notes() {
1661 let sender = note.metadata().sender();
1662 if sender != account_id {
1663 return Err(ClientError::TransactionRequestError(
1664 TransactionRequestError::OutputNoteSenderMismatch {
1665 expected: account_id,
1666 actual: sender,
1667 },
1668 ));
1669 }
1670 }
1671
1672 Ok(())
1673}
1674
1675fn validate_basic_account_request(
1678 transaction_request: &TransactionRequest,
1679 vault_assets: &[Asset],
1680) -> Result<(), ClientError> {
1681 let (fungible_balance_map, non_fungible_set) = get_outgoing_assets(transaction_request);
1683
1684 let (incoming_fungible_balance_map, incoming_non_fungible_balance_set) =
1686 transaction_request.incoming_assets();
1687
1688 let mut available_fungible: BTreeMap<AccountId, u64> = BTreeMap::new();
1691 for asset in vault_assets {
1692 if let Asset::Fungible(fungible) = asset {
1693 let balance = available_fungible.entry(fungible.faucet_id()).or_default();
1694 *balance = balance.saturating_add(fungible.amount().as_u64());
1695 }
1696 }
1697
1698 for (faucet_id, amount) in fungible_balance_map {
1701 let account_asset_amount = available_fungible.get(&faucet_id).copied().unwrap_or(0);
1702 let incoming_balance = incoming_fungible_balance_map.get(&faucet_id).unwrap_or(&0);
1703 if account_asset_amount + incoming_balance < amount {
1704 return Err(ClientError::AssetError(AssetError::FungibleAssetAmountNotSufficient {
1705 minuend: account_asset_amount,
1706 subtrahend: amount,
1707 }));
1708 }
1709 }
1710
1711 for non_fungible in &non_fungible_set {
1714 let held = vault_assets
1715 .iter()
1716 .any(|asset| matches!(asset, Asset::NonFungible(nf) if nf == non_fungible));
1717 if !held && !incoming_non_fungible_balance_set.contains(non_fungible) {
1718 return Err(ClientError::TransactionRequestError(
1719 TransactionRequestError::MissingNonFungibleAsset(non_fungible.faucet_id()),
1720 ));
1721 }
1722 }
1723
1724 Ok(())
1725}
1726
1727pub(crate) async fn fetch_public_account_inputs(
1737 store: &Arc<dyn Store>,
1738 rpc_api: &Arc<dyn NodeRpcClient>,
1739 account_id: AccountId,
1740 storage_requirements: AccountStorageRequirements,
1741 account_state_at: AccountStateAt,
1742) -> Result<AccountInputs, ClientError> {
1743 let known_code: Option<AccountCode> =
1744 store.get_foreign_account_code(vec![account_id]).await?.into_values().next();
1745
1746 let vault = store
1749 .get_account_header(account_id)
1750 .await?
1751 .map_or(VaultFetch::Always, |(header, ..)| {
1752 VaultFetch::IfChangedFrom(header.vault_root())
1753 });
1754
1755 let (_block_num, account_proof) = rpc_api
1756 .get_account(
1757 account_id,
1758 GetAccountRequest::new()
1759 .with_storage(StorageMapFetch::Slots(storage_requirements.clone()))
1760 .at(account_state_at)
1761 .with_known_code(known_code)
1762 .with_vault(vault),
1763 )
1764 .await?;
1765
1766 let account_inputs = request::account_proof_into_inputs(account_proof)?;
1767
1768 let _ = store
1769 .upsert_foreign_account_code(account_id, account_inputs.code().clone())
1770 .await
1771 .inspect_err(|err| {
1772 tracing::warn!(
1773 %account_id,
1774 %err,
1775 "Failed to persist foreign account code to store"
1776 );
1777 });
1778
1779 Ok(account_inputs)
1780}
1781
1782pub fn notes_from_output(output_notes: &RawOutputNotes) -> impl Iterator<Item = &Note> {
1787 output_notes.iter().filter_map(|n| match n {
1788 RawOutputNote::Full(n) => Some(n),
1789 RawOutputNote::Partial(_) => None,
1790 })
1791}
1792
1793pub(crate) fn validate_executed_transaction(
1796 executed_transaction: &ExecutedTransaction,
1797 expected_output_recipients: &[NoteRecipient],
1798) -> Result<(), ClientError> {
1799 let tx_output_recipient_digests = executed_transaction
1800 .output_notes()
1801 .iter()
1802 .filter_map(|n| n.recipient().map(NoteRecipient::digest))
1803 .collect::<Vec<_>>();
1804
1805 let missing_recipient_digest: Vec<Word> = expected_output_recipients
1806 .iter()
1807 .filter_map(|recipient| {
1808 (!tx_output_recipient_digests.contains(&recipient.digest()))
1809 .then_some(recipient.digest())
1810 })
1811 .collect();
1812
1813 if !missing_recipient_digest.is_empty() {
1814 return Err(ClientError::MissingOutputRecipients(missing_recipient_digest));
1815 }
1816
1817 Ok(())
1818}
1819
1820#[cfg(test)]
1824mod tests {
1825 use alloc::vec;
1826
1827 use miden_protocol::Word;
1828 use miden_protocol::account::auth::AuthSecretKey;
1829 use miden_protocol::account::{
1830 Account,
1831 AccountBuilder,
1832 AccountComponent,
1833 AccountComponentMetadata,
1834 AccountId,
1835 AccountType,
1836 };
1837 use miden_protocol::asset::FungibleAsset;
1838 use miden_protocol::block::{BlockHeader, BlockNumber, FeeParameters};
1839 use miden_protocol::crypto::rand::RandomCoin;
1840 use miden_protocol::note::{Note, NoteType};
1841 use miden_protocol::testing::account_id::{
1842 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
1843 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
1844 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
1845 ACCOUNT_ID_SENDER,
1846 };
1847 use miden_protocol::testing::validator_keys::random_validator_set;
1848 use miden_standards::account::AccountBuilderSchemaCommitmentExt;
1849 use miden_standards::account::auth::{
1850 Approver,
1851 ApproverSet,
1852 AuthGuardedMultisig,
1853 AuthGuardedMultisigConfig,
1854 AuthMultisig,
1855 AuthMultisigConfig,
1856 AuthMultisigSmart,
1857 AuthMultisigSmartConfig,
1858 AuthSingleSig,
1859 FeeConversionInfo,
1860 GuardianConfig,
1861 NoAuth,
1862 commit_fee_conversion_info,
1863 };
1864 use miden_standards::account::wallets::BasicWallet;
1865 use miden_standards::note::P2idNote;
1866
1867 use super::{
1868 AccountComponentInterface,
1869 NATIVE_FEE_CONVERSION_SALT,
1870 TransactionRequest,
1871 TransactionRequestBuilder,
1872 attach_native_fee_conversion_info,
1873 validate_fee_conversion_info_support,
1874 validate_output_note_senders,
1875 };
1876 use crate::ClientError;
1877 use crate::assembly::CodeBuilder;
1878 use crate::auth::AuthSchemeId;
1879 use crate::transaction::TransactionRequestError;
1880
1881 fn own_note_with_sender(sender: AccountId) -> Note {
1882 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
1883 let target_id =
1884 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1885 let mut rng = RandomCoin::new(Word::default());
1886
1887 P2idNote::builder()
1888 .sender(sender)
1889 .target(target_id)
1890 .asset(FungibleAsset::new(faucet_id, 100).unwrap())
1891 .note_type(NoteType::Public)
1892 .generate_serial_number(&mut rng)
1893 .build()
1894 .expect("note creation failed")
1895 .into()
1896 }
1897
1898 #[test]
1899 fn output_note_with_foreign_sender_is_rejected() {
1900 let account_id =
1901 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1902 let foreign_sender = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
1903 assert_ne!(account_id, foreign_sender);
1904
1905 let request = TransactionRequestBuilder::new()
1906 .own_output_notes(vec![own_note_with_sender(foreign_sender)])
1907 .build()
1908 .unwrap();
1909
1910 let err = validate_output_note_senders(&request, account_id).unwrap_err();
1911 match err {
1912 ClientError::TransactionRequestError(
1913 TransactionRequestError::OutputNoteSenderMismatch { expected, actual },
1914 ) => {
1915 assert_eq!(expected, account_id);
1916 assert_eq!(actual, foreign_sender);
1917 },
1918 other => panic!("expected OutputNoteSenderMismatch, got {other:?}"),
1919 }
1920 }
1921
1922 #[test]
1923 fn output_note_with_matching_sender_is_accepted() {
1924 let account_id =
1925 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1926
1927 let request = TransactionRequestBuilder::new()
1928 .own_output_notes(vec![own_note_with_sender(account_id)])
1929 .build()
1930 .unwrap();
1931
1932 validate_output_note_senders(&request, account_id).unwrap();
1933 }
1934
1935 #[test]
1936 fn request_without_own_output_notes_is_accepted() {
1937 let account_id =
1938 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1939 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
1940
1941 let request = TransactionRequestBuilder::new()
1943 .input_notes(vec![(own_note_with_sender(faucet_id), None)])
1944 .build()
1945 .unwrap();
1946
1947 validate_output_note_senders(&request, account_id).unwrap();
1948 }
1949
1950 fn account_with_auth(auth_component: impl Into<AccountComponent>) -> Account {
1952 AccountBuilder::new([7u8; 32])
1953 .account_type(AccountType::Public)
1954 .with_component(auth_component)
1955 .with_component(BasicWallet)
1956 .build_with_schema_commitment()
1957 .expect("account creation failed")
1958 }
1959
1960 fn fee_conversion_request() -> TransactionRequest {
1961 TransactionRequestBuilder::new()
1962 .fee_conversion_salt(Word::from([13u32, 14, 15, 16]))
1963 .build()
1964 .unwrap()
1965 }
1966
1967 #[test]
1968 fn fee_conversion_info_is_accepted_by_a_signature_authenticated_account() {
1969 let key = AuthSecretKey::new_falcon512_poseidon2();
1970 let auth = AuthSingleSig::new(Approver::new(
1971 key.public_key().to_commitment(),
1972 AuthSchemeId::Falcon512Poseidon2,
1973 ));
1974
1975 validate_fee_conversion_info_support(
1976 &fee_conversion_request(),
1977 &account_with_auth(auth).code_interface(),
1978 )
1979 .unwrap();
1980 }
1981
1982 #[test]
1983 fn fee_conversion_info_is_rejected_by_an_account_that_cannot_read_it() {
1984 let account = account_with_auth(NoAuth);
1985
1986 let err = validate_fee_conversion_info_support(
1987 &fee_conversion_request(),
1988 &account.code_interface(),
1989 )
1990 .expect_err("NoAuth does not read the auth args");
1991 match err {
1992 ClientError::TransactionRequestError(
1993 TransactionRequestError::FeeConversionInfoUnsupported(auth_component),
1994 ) => assert_eq!(auth_component, AccountComponentInterface::AuthNoAuth.name()),
1995 other => panic!("expected FeeConversionInfoUnsupported, got {other:?}"),
1996 }
1997 }
1998
1999 #[test]
2000 fn a_request_without_fee_conversion_info_skips_the_auth_component_check() {
2001 validate_fee_conversion_info_support(
2003 &TransactionRequestBuilder::new().build().unwrap(),
2004 &account_with_auth(NoAuth).code_interface(),
2005 )
2006 .unwrap();
2007 }
2008
2009 const NATIVE_FEE_FAUCET: u128 = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET;
2015
2016 fn header_with_base_fee(verification_base_fee: u32) -> BlockHeader {
2019 let fee_parameters = FeeParameters::new(
2020 AccountId::try_from(NATIVE_FEE_FAUCET).unwrap(),
2021 verification_base_fee,
2022 );
2023 let (_, validator_keys) = random_validator_set(1);
2024
2025 BlockHeader::new(
2026 1,
2027 Word::empty(),
2028 BlockNumber::from(1u32),
2029 Word::empty(),
2030 Word::empty(),
2031 Word::empty(),
2032 Word::empty(),
2033 Word::empty(),
2034 Word::empty(),
2035 validator_keys,
2036 fee_parameters,
2037 0,
2038 )
2039 }
2040
2041 fn injected_auth_arg(
2044 mut request: TransactionRequest,
2045 account: &Account,
2046 verification_base_fee: u32,
2047 ) -> Option<Word> {
2048 let _ = attach_native_fee_conversion_info(
2049 &mut request,
2050 &account.code_interface(),
2051 &header_with_base_fee(verification_base_fee),
2052 );
2053 *request.auth_arg()
2054 }
2055
2056 fn try_injected_auth_arg(
2058 mut request: TransactionRequest,
2059 account: &Account,
2060 verification_base_fee: u32,
2061 ) -> Result<Option<Word>, ClientError> {
2062 attach_native_fee_conversion_info(
2063 &mut request,
2064 &account.code_interface(),
2065 &header_with_base_fee(verification_base_fee),
2066 )?;
2067 Ok(*request.auth_arg())
2068 }
2069
2070 fn singlesig_account() -> Account {
2071 let key = AuthSecretKey::new_falcon512_poseidon2();
2072 account_with_auth(AuthSingleSig::new(Approver::new(
2073 key.public_key().to_commitment(),
2074 AuthSchemeId::Falcon512Poseidon2,
2075 )))
2076 }
2077
2078 fn guarded_multisig_account() -> Account {
2079 let approvers = ApproverSet::new(
2080 vec![Approver::new(
2081 AuthSecretKey::new_falcon512_poseidon2().public_key().to_commitment(),
2082 AuthSchemeId::Falcon512Poseidon2,
2083 )],
2084 1,
2085 )
2086 .unwrap();
2087
2088 let guardian = GuardianConfig::new(Approver::new(
2089 AuthSecretKey::new_falcon512_poseidon2().public_key().to_commitment(),
2090 AuthSchemeId::Falcon512Poseidon2,
2091 ));
2092
2093 account_with_auth(
2094 AuthGuardedMultisig::new(AuthGuardedMultisigConfig::new(approvers, guardian).unwrap())
2095 .unwrap(),
2096 )
2097 }
2098
2099 #[test]
2100 fn native_fee_conversion_info_is_attached_on_a_fee_charging_chain() {
2101 let auth_arg = injected_auth_arg(
2102 TransactionRequestBuilder::new().build().unwrap(),
2103 &singlesig_account(),
2104 500,
2105 )
2106 .expect("a fee-charging chain should get conversion info attached");
2107
2108 let (expected, _) = commit_fee_conversion_info(
2109 FeeConversionInfo::one_to_one(AccountId::try_from(NATIVE_FEE_FAUCET).unwrap()),
2110 NATIVE_FEE_CONVERSION_SALT,
2111 );
2112 assert_eq!(auth_arg, expected, "the fee should be paid in the native asset at rate 1/1");
2113 }
2114
2115 #[test]
2116 fn an_explicit_auth_arg_is_not_overwritten() {
2117 let auth_arg = Word::from([21u32, 22, 23, 24]);
2118 let request = TransactionRequestBuilder::new().auth_arg(auth_arg).build().unwrap();
2119
2120 assert_eq!(
2121 injected_auth_arg(request, &singlesig_account(), 500),
2122 Some(auth_arg),
2123 "a request that declares its own auth arg keeps it"
2124 );
2125 }
2126
2127 fn native_commitment(salt: Word) -> Word {
2129 let (auth_arg, _) = commit_fee_conversion_info(
2130 FeeConversionInfo::one_to_one(AccountId::try_from(NATIVE_FEE_FAUCET).unwrap()),
2131 salt,
2132 );
2133 auth_arg
2134 }
2135
2136 #[test]
2137 fn a_declared_salt_is_used_for_the_native_commitment() {
2138 let salt = Word::from([17u32, 18, 19, 20]);
2139 let request = TransactionRequestBuilder::new().fee_conversion_salt(salt).build().unwrap();
2140
2141 let auth_arg = injected_auth_arg(request, &singlesig_account(), 500)
2142 .expect("a declared salt should still get native conversion info attached");
2143
2144 assert_eq!(auth_arg, native_commitment(salt));
2145 }
2146
2147 fn multisig_account() -> Account {
2148 let approvers = ApproverSet::new(
2149 vec![Approver::new(
2150 AuthSecretKey::new_falcon512_poseidon2().public_key().to_commitment(),
2151 AuthSchemeId::Falcon512Poseidon2,
2152 )],
2153 1,
2154 )
2155 .unwrap();
2156
2157 account_with_auth(AuthMultisig::new(AuthMultisigConfig::new(approvers)).unwrap())
2158 }
2159
2160 #[test]
2161 fn nothing_is_attached_where_it_is_not_needed_or_not_readable() {
2162 for (case, account, base_fee) in [
2163 ("a zero base fee charges nothing", singlesig_account(), 0),
2164 ("NoAuth never reads the auth args", account_with_auth(NoAuth), 500),
2165 ("a multisig salt is its own replay guard", multisig_account(), 500),
2166 ] {
2167 assert_eq!(
2168 injected_auth_arg(
2169 TransactionRequestBuilder::new().build().unwrap(),
2170 &account,
2171 base_fee
2172 ),
2173 None,
2174 "{case}"
2175 );
2176 }
2177 }
2178
2179 #[test]
2186 fn fee_conversion_info_is_accepted_by_a_guarded_multisig_account() {
2187 validate_fee_conversion_info_support(
2188 &fee_conversion_request(),
2189 &guarded_multisig_account().code_interface(),
2190 )
2191 .expect("a guarded multisig reads the auth args as conversion info");
2192 }
2193
2194 #[test]
2198 fn a_guarded_multisig_account_must_declare_its_own_fee_conversion_info() {
2199 let err = try_injected_auth_arg(
2200 TransactionRequestBuilder::new().build().unwrap(),
2201 &guarded_multisig_account(),
2202 500,
2203 )
2204 .expect_err("a guarded multisig account cannot inherit the fixed native salt");
2205 match err {
2206 ClientError::TransactionRequestError(
2207 TransactionRequestError::FeeConversionInfoRequired(auth_component),
2208 ) => {
2209 assert_eq!(auth_component, AccountComponentInterface::AuthGuardedMultisig.name());
2210 },
2211 other => panic!("expected FeeConversionInfoRequired, got {other:?}"),
2212 }
2213
2214 let salt = Word::from([13u32, 14, 15, 16]);
2215 assert_eq!(
2216 try_injected_auth_arg(fee_conversion_request(), &guarded_multisig_account(), 500)
2217 .expect("a declared salt is accepted"),
2218 Some(native_commitment(salt)),
2219 "a guarded multisig account that declares a salt commits the native conversion info"
2220 );
2221
2222 assert_eq!(
2223 try_injected_auth_arg(
2224 TransactionRequestBuilder::new().build().unwrap(),
2225 &guarded_multisig_account(),
2226 0
2227 )
2228 .expect("a chain charging nothing needs no conversion info"),
2229 None,
2230 );
2231 }
2232
2233 fn smart_multisig_account() -> Account {
2237 let approvers = ApproverSet::new(
2238 vec![Approver::new(
2239 AuthSecretKey::new_falcon512_poseidon2().public_key().to_commitment(),
2240 AuthSchemeId::Falcon512Poseidon2,
2241 )],
2242 1,
2243 )
2244 .unwrap();
2245
2246 account_with_auth(AuthMultisigSmart::new(AuthMultisigSmartConfig::new(approvers)).unwrap())
2247 }
2248
2249 #[test]
2254 fn fee_conversion_info_is_accepted_by_a_smart_multisig_account() {
2255 validate_fee_conversion_info_support(
2256 &fee_conversion_request(),
2257 &smart_multisig_account().code_interface(),
2258 )
2259 .expect("a smart multisig reads the auth args as conversion info");
2260 }
2261
2262 #[test]
2266 fn a_smart_multisig_account_must_declare_its_own_fee_conversion_info() {
2267 let err = try_injected_auth_arg(
2268 TransactionRequestBuilder::new().build().unwrap(),
2269 &smart_multisig_account(),
2270 500,
2271 )
2272 .expect_err("a smart multisig account cannot inherit the fixed native salt");
2273 match err {
2274 ClientError::TransactionRequestError(
2275 TransactionRequestError::FeeConversionInfoRequired(auth_component),
2276 ) => {
2277 assert_eq!(auth_component, AccountComponentInterface::AuthMultisigSmart.name());
2278 },
2279 other => panic!("expected FeeConversionInfoRequired, got {other:?}"),
2280 }
2281
2282 let salt = Word::from([13u32, 14, 15, 16]);
2283 assert_eq!(
2284 try_injected_auth_arg(fee_conversion_request(), &smart_multisig_account(), 500)
2285 .expect("a declared salt is accepted"),
2286 Some(native_commitment(salt)),
2287 "a smart multisig account that declares a salt commits the native conversion info"
2288 );
2289
2290 assert_eq!(
2291 try_injected_auth_arg(
2292 TransactionRequestBuilder::new().build().unwrap(),
2293 &smart_multisig_account(),
2294 0
2295 )
2296 .expect("a chain charging nothing needs no conversion info"),
2297 None,
2298 );
2299 }
2300
2301 #[test]
2304 fn an_unrecognized_auth_component_is_rejected_rather_than_panicking() {
2305 const CUSTOM_AUTH: &str = "
2306 use miden::protocol::native_account
2307
2308 @auth_script
2309 pub proc auth_custom
2310 exec.native_account::incr_nonce
2311 drop
2312 end
2313 ";
2314
2315 let code = CodeBuilder::default()
2316 .compile_component_code("miden::testing::custom_auth", CUSTOM_AUTH)
2317 .expect("custom auth component code should compile");
2318 let auth = AccountComponent::new(
2319 code,
2320 vec![],
2321 AccountComponentMetadata::new("miden::testing::custom_auth"),
2322 )
2323 .expect("custom auth component");
2324
2325 let account = account_with_auth(auth);
2326
2327 let err = validate_fee_conversion_info_support(
2328 &fee_conversion_request(),
2329 &account.code_interface(),
2330 )
2331 .expect_err("an account with no recognized auth component cannot read conversion info");
2332 assert!(matches!(
2333 err,
2334 ClientError::TransactionRequestError(
2335 TransactionRequestError::FeeConversionInfoUnsupported(_)
2336 )
2337 ));
2338
2339 assert_eq!(
2340 try_injected_auth_arg(TransactionRequestBuilder::new().build().unwrap(), &account, 500)
2341 .expect("a request declaring nothing is left alone"),
2342 None,
2343 "an auth component nothing can reason about gets nothing attached"
2344 );
2345 }
2346}