1use alloc::boxed::Box;
66use alloc::collections::{BTreeMap, BTreeSet};
67use alloc::string::String;
68use alloc::sync::Arc;
69use alloc::vec::Vec;
70
71use miden_protocol::account::{AccountCode, AccountCodeInterface, AccountId, PartialAccount};
72use miden_protocol::asset::Asset;
73use miden_protocol::block::account_tree::AccountWitness;
74use miden_protocol::block::{BlockHeader, BlockNumber, FeeParameters};
75use miden_protocol::errors::AssetError;
76use miden_protocol::note::{
77 Note,
78 NoteAttachments,
79 NoteDetails,
80 NoteId,
81 NoteRecipient,
82 NoteScript,
83 NoteTag,
84};
85use miden_protocol::protocol_config::ProtocolConfig;
86use miden_protocol::transaction::PartialBlockchain;
87use miden_protocol::vm::MIN_STACK_DEPTH;
88use miden_protocol::{Felt, Word};
89use miden_standards::account::auth::FeeConversionInfo;
90use miden_standards::account::faucets::FungibleFaucet;
91use miden_standards::account::interface::AccountComponentInterfaceExt;
92use miden_standards::note::TxFeeNote;
93use miden_tx::{DataStore, NoteConsumptionChecker, TransactionExecutor};
94use tracing::info;
95
96use super::Client;
97use crate::ClientError;
98use crate::note::{NoteScreenerError, NoteUpdateTracker, StandardNote};
99use crate::rpc::domain::account::{
100 AccountStorageRequirements,
101 GetAccountRequest,
102 StorageMapFetch,
103 VaultFetch,
104};
105use crate::rpc::encryption::{TransactionEncryptionKey, seal_transaction_inputs};
106use crate::rpc::{AccountStateAt, NodeRpcClient, RpcError};
107use crate::store::data_store::{ClientDataStore, build_partial_mmr_and_headers_with_fallback};
108use crate::store::input_note_states::ExpectedNoteState;
109use crate::store::{
110 AccountRecord,
111 InputNoteRecord,
112 InputNoteState,
113 NoteFilter,
114 NoteRecordError,
115 OutputNoteRecord,
116 Store,
117 StoreError,
118 TransactionFilter,
119};
120use crate::sync::NoteTagRecord;
121
122pub mod batch;
123pub use batch::{BatchBuilder, BatchBuilderError, ProvenBatchSubmission};
124
125mod chain_anchor;
126pub use chain_anchor::{ChainAnchor, ChainAnchorError};
127
128#[cfg(feature = "dap")]
129mod dap_executor;
130mod prover;
131pub use prover::TransactionProver;
132
133mod record;
134pub use record::{
135 DiscardCause,
136 TransactionDetails,
137 TransactionRecord,
138 TransactionStatus,
139 TransactionStatusVariant,
140};
141
142mod store_update;
143pub use store_update::TransactionStoreUpdate;
144
145mod request;
146pub use request::{
147 ForeignAccount,
148 NoteArgs,
149 PaymentNoteDescription,
150 PswapTransactionData,
151 SwapTransactionData,
152 TransactionRequest,
153 TransactionRequestBuilder,
154 TransactionRequestError,
155 TransactionScriptTemplate,
156 build_fpi_script,
157};
158
159mod observer;
160pub use observer::TransactionObserver;
161
162mod result;
163pub use miden_protocol::transaction::{
166 AccountInputs,
167 ExecutedTransaction,
168 InputNote,
169 InputNotes,
170 OutputNote,
171 OutputNotes,
172 ProvenTransaction,
173 PublicOutputNote,
174 RawOutputNote,
175 RawOutputNotes,
176 TransactionArgs,
177 TransactionFee,
178 TransactionFeeError,
179 TransactionId,
180 TransactionInputs,
181 TransactionKernel,
182 TransactionScript,
183 TransactionScriptRoot,
184 TransactionSummary,
185};
186pub use miden_protocol::vm::{AdviceInputs, AdviceMap};
187pub use miden_standards::account::interface::{AccountComponentInterface, AccountInterface};
188pub use miden_standards::tx_script::{
189 ExpirationTransactionScript,
190 SendNotesTransactionScriptError,
191};
192pub use miden_tx::auth::TransactionAuthenticator;
193pub use miden_tx::{
194 DataStoreError,
195 LocalTransactionProver,
196 Prover,
197 TransactionExecutorError,
198 TransactionProverError,
199};
200pub use result::TransactionResult;
201
202pub(crate) const NATIVE_FEE_CONVERSION_SALT: Word = Word::empty();
209
210impl<AUTH> Client<AUTH>
212where
213 AUTH: TransactionAuthenticator + Sync + 'static,
214{
215 pub async fn get_transactions(
220 &self,
221 filter: TransactionFilter,
222 ) -> Result<Vec<TransactionRecord>, ClientError> {
223 self.store.get_transactions(filter).await.map_err(Into::into)
224 }
225
226 pub async fn submit_new_transaction(
234 &mut self,
235 account_id: AccountId,
236 transaction_request: TransactionRequest,
237 ) -> Result<TransactionId, ClientError> {
238 let prover = self.tx_prover.clone();
239 self.submit_new_transaction_with_prover(account_id, transaction_request, prover)
240 .await
241 }
242
243 pub async fn submit_new_transaction_with_prover(
249 &mut self,
250 account_id: AccountId,
251 transaction_request: TransactionRequest,
252 tx_prover: Arc<dyn TransactionProver>,
253 ) -> Result<TransactionId, ClientError> {
254 if !transaction_request.expected_ntx_scripts().is_empty() {
257 Box::pin(self.ensure_ntx_scripts_registered(
258 account_id,
259 transaction_request.expected_ntx_scripts(),
260 tx_prover.clone(),
261 ))
262 .await?;
263 }
264
265 let tx_result = self.execute_transaction(account_id, transaction_request).await?;
266 let tx_id = tx_result.executed_transaction().id();
267
268 let proven_transaction = self.prove_transaction_with(&tx_result, tx_prover).await?;
269 let submission_height =
270 self.submit_proven_transaction(proven_transaction, &tx_result).await?;
271
272 let tx_update =
280 Box::new(self.get_transaction_store_update(&tx_result, submission_height).await?);
281
282 if let Err(apply_err) = self.apply_transaction_update((*tx_update).clone()).await {
283 info!(
284 "apply_transaction_update failed for submitted tx {tx_id}; returning \
285 ApplyTransactionAfterSubmitFailed with the pending update attached: {apply_err}"
286 );
287 return Err(ClientError::ApplyTransactionAfterSubmitFailed {
288 pending_update: tx_update,
289 source: Box::new(apply_err),
290 });
291 }
292
293 for observer in &self.transaction_observers {
297 crate::errors::log_observer_failure(
298 observer.name(),
299 "TransactionObserver::apply",
300 observer.apply(&tx_result).await,
301 );
302 }
303
304 Ok(tx_id)
305 }
306
307 pub async fn execute_transaction(
317 &self,
318 account_id: AccountId,
319 transaction_request: TransactionRequest,
320 ) -> Result<TransactionResult, ClientError> {
321 Box::pin(self.execute_transaction_with_mode(
322 account_id,
323 transaction_request,
324 TransactionExecutionMode::Standard,
325 None,
326 ))
327 .await
328 }
329
330 pub async fn execute_transaction_at(
364 &mut self,
365 account_id: AccountId,
366 transaction_request: TransactionRequest,
367 anchor: ChainAnchor,
368 ) -> Result<TransactionResult, ClientError> {
369 let result = self
370 .execute_transaction_with_mode(
371 account_id,
372 transaction_request,
373 TransactionExecutionMode::Standard,
374 Some(Box::new(anchor)),
375 )
376 .await?;
377
378 let expiration = result.executed_transaction().expiration_block_num();
383 let sync_height = self.store.get_sync_height().await?;
384 if expiration <= sync_height {
385 return Err(
386 ChainAnchorError::AnchoredTransactionExpired { expiration, sync_height }.into()
387 );
388 }
389
390 Ok(result)
391 }
392
393 async fn chain_anchor_at_tip(
398 &self,
399 tracked_blocks: BTreeSet<BlockNumber>,
400 ) -> Result<ChainAnchor, ClientError> {
401 let sync_height = self.store.get_sync_height().await?;
402
403 let (header, _had_notes) = self
404 .store
405 .get_block_header_by_num(sync_height)
406 .await?
407 .ok_or(StoreError::BlockHeaderNotFound(sync_height))?;
408
409 let mut tracked_blocks = tracked_blocks;
410 tracked_blocks.remove(&sync_height);
412 if let Some(&future_block) =
413 tracked_blocks.iter().find(|&&block_num| block_num > sync_height)
414 {
415 return Err(StoreError::BlockHeaderNotFound(future_block).into());
416 }
417
418 let peaks = self.store.get_current_blockchain_peaks().await?;
419 let (partial_mmr, block_headers) = build_partial_mmr_and_headers_with_fallback(
420 &self.store,
421 &self.rpc_api,
422 peaks,
423 &tracked_blocks,
424 )
425 .await?;
426
427 let chain = PartialBlockchain::new(partial_mmr, block_headers)?;
428
429 Ok(ChainAnchor::new(header, chain)?)
430 }
431
432 pub async fn chain_anchor_for_request(
451 &self,
452 transaction_request: &TransactionRequest,
453 ) -> Result<ChainAnchor, ClientError> {
454 let inferred_input_note_ids: Vec<NoteId> = transaction_request
455 .input_note_ids()
456 .filter(|note_id| !transaction_request.explicit_input_notes.contains_key(note_id))
457 .collect();
458
459 let mut tracked_blocks: BTreeSet<BlockNumber> = if inferred_input_note_ids.is_empty() {
460 BTreeSet::new()
461 } else {
462 self.store
463 .get_input_notes(NoteFilter::List(inferred_input_note_ids))
464 .await?
465 .iter()
466 .filter(|record| record.is_authenticated())
467 .filter_map(|record| record.inclusion_proof())
468 .map(|proof| proof.location().block_num())
469 .collect()
470 };
471 tracked_blocks.extend(
472 transaction_request
473 .explicit_input_notes
474 .values()
475 .filter_map(InputNote::proof)
476 .map(|proof| proof.location().block_num()),
477 );
478 tracked_blocks.extend(transaction_request.block_numbers().iter().copied());
479
480 self.chain_anchor_at_tip(tracked_blocks).await
481 }
482
483 #[cfg(feature = "dap")]
497 pub async fn execute_transaction_with_dap(
498 &self,
499 account_id: AccountId,
500 transaction_request: TransactionRequest,
501 ) -> Result<TransactionResult, ClientError> {
502 self.execute_transaction_with_mode(
503 account_id,
504 transaction_request,
505 TransactionExecutionMode::Dap,
506 None,
507 )
508 .await
509 }
510
511 async fn execute_transaction_with_mode(
515 &self,
516 account_id: AccountId,
517 transaction_request: TransactionRequest,
518 execution_mode: TransactionExecutionMode,
519 anchor: Option<Box<ChainAnchor>>,
520 ) -> Result<TransactionResult, ClientError> {
521 let account: PartialAccount =
522 self.get_native_account_record(account_id).await?.try_into()?;
523
524 let prep = self
525 .prepare_transaction(&account, transaction_request, anchor.as_deref())
526 .await?;
527
528 let mut data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
529 if let Some(anchor) = anchor {
530 data_store = data_store.with_chain_anchor(*anchor);
531 }
532 data_store.register_note_scripts(prep.output_note_scripts());
533 data_store.register_block_numbers(prep.block_numbers.iter().copied());
534 for fpi_account in &prep.foreign_account_inputs {
535 data_store.mast_store().load_account_code(fpi_account.code());
536 }
537 data_store.register_foreign_account_inputs(prep.foreign_account_inputs);
538
539 data_store.mast_store().load_account_code(account.code());
540
541 let mut notes = prep.notes;
542 if prep.ignore_invalid_notes {
543 notes = self
544 .get_valid_input_notes(
545 &data_store,
546 account.id(),
547 prep.block_num,
548 notes,
549 prep.tx_args.clone(),
550 )
551 .await?;
552 }
553
554 let executed_transaction = match execution_mode {
555 TransactionExecutionMode::Standard => {
556 self.build_executor(&data_store)?
557 .execute_transaction(account_id, prep.block_num, notes, prep.tx_args)
558 .await?
559 },
560 #[cfg(feature = "dap")]
561 TransactionExecutionMode::Dap => {
562 self.build_dap_executor(&data_store)?
563 .execute_transaction(account_id, prep.block_num, notes, prep.tx_args)
564 .await?
565 },
566 };
567
568 validate_executed_transaction(&executed_transaction, &prep.output_recipients)?;
569 TransactionResult::new(executed_transaction, prep.future_notes)
570 }
571
572 pub(crate) async fn prepare_transaction(
588 &self,
589 account: &PartialAccount,
590 transaction_request: TransactionRequest,
591 anchor: Option<&ChainAnchor>,
592 ) -> Result<PreparedTransaction, ClientError> {
593 self.validate_account_request(
594 &transaction_request,
595 account.id(),
596 &account.code_interface(),
597 )
598 .await?;
599
600 self.prepare_transaction_inner(account.code_interface(), transaction_request, anchor)
601 .await
602 }
603
604 pub(crate) async fn prepare_transaction_for_batch(
605 &self,
606 account: &PartialAccount,
607 transaction_request: TransactionRequest,
608 ) -> Result<PreparedTransaction, ClientError> {
609 self.prepare_transaction_inner(account.code_interface(), transaction_request, None)
610 .await
611 }
612
613 async fn prepare_transaction_inner(
614 &self,
615 account_code_interface: AccountCodeInterface,
616 mut transaction_request: TransactionRequest,
617 anchor: Option<&ChainAnchor>,
618 ) -> Result<PreparedTransaction, ClientError> {
619 if anchor.is_none() {
620 self.validate_recency().await?;
621 }
622
623 let mut stored_note_records = self
625 .store
626 .get_input_notes(NoteFilter::List(transaction_request.input_note_ids().collect()))
627 .await?;
628
629 for note in &stored_note_records {
634 if note.is_consumed() {
635 return Err(ClientError::TransactionRequestError(
636 TransactionRequestError::InputNoteAlreadyConsumed(note.details_commitment()),
637 ));
638 }
639 if let Some(transaction_id) = note.consumer_transaction_id()
640 && note.is_processing()
641 {
642 return Err(ClientError::TransactionRequestError(
643 TransactionRequestError::InputNoteBeingProcessed {
644 note: note.details_commitment(),
645 transaction_id: *transaction_id,
646 },
647 ));
648 }
649 }
650
651 stored_note_records.retain(InputNoteRecord::is_authenticated);
653
654 let notes = transaction_request.build_input_notes(stored_note_records)?;
655
656 if let Some(anchor) = anchor {
660 for note in notes.iter() {
661 if let Some(location) = note.location() {
662 let block_num = location.block_num();
663 if block_num < anchor.block_num()
664 && !anchor.partial_blockchain().contains_block(block_num)
665 {
666 return Err(ChainAnchorError::BlockNotTracked { block_num }.into());
667 }
668 }
669 }
670 }
671
672 let output_recipients =
673 transaction_request.expected_output_recipients().cloned().collect::<Vec<_>>();
674
675 let future_notes: Vec<(NoteDetails, NoteTag)> =
676 transaction_request.expected_future_notes().cloned().collect();
677
678 let tx_script = transaction_request.build_transaction_script(&account_code_interface)?;
679
680 let foreign_accounts = transaction_request.foreign_accounts().clone();
681
682 let block_num = match anchor {
685 Some(anchor) => anchor.block_num(),
686 None => self.store.get_sync_height().await?,
687 };
688
689 let foreign_account_inputs = self
690 .get_foreign_account_inputs(foreign_accounts.into_values(), block_num)
691 .await?;
692
693 let ignore_invalid_notes = transaction_request.ignore_invalid_input_notes();
694 let block_numbers = transaction_request.block_numbers().clone();
695
696 let reference_header = match anchor {
697 Some(anchor) => anchor.header().clone(),
698 None => {
699 self.store
700 .get_block_header_by_num(block_num)
701 .await?
702 .ok_or(StoreError::BlockHeaderNotFound(block_num))?
703 .0
704 },
705 };
706
707 for inputs in &foreign_account_inputs {
710 if inputs.compute_account_root().ok() != Some(reference_header.account_root()) {
711 return Err(TransactionRequestError::ForeignAccountNotAtReferenceBlock {
712 account_id: inputs.id(),
713 block_num,
714 }
715 .into());
716 }
717 }
718
719 attach_native_fee_conversion_info(
720 &mut transaction_request,
721 &account_code_interface,
722 &reference_header,
723 &self.get_protocol_config(reference_header.protocol_config_commitment()).await?,
724 )?;
725
726 let tx_args = transaction_request.into_transaction_args(tx_script);
727
728 Ok(PreparedTransaction {
729 notes,
730 output_recipients,
731 future_notes,
732 tx_args,
733 foreign_account_inputs,
734 block_numbers,
735 block_num,
736 ignore_invalid_notes,
737 })
738 }
739
740 pub async fn prove_transaction(
742 &self,
743 tx_result: &TransactionResult,
744 ) -> Result<ProvenTransaction, ClientError> {
745 self.prove_transaction_with(tx_result, self.tx_prover.clone()).await
746 }
747
748 pub async fn prove_transaction_with(
756 &self,
757 tx_result: &TransactionResult,
758 tx_prover: Arc<dyn TransactionProver>,
759 ) -> Result<ProvenTransaction, ClientError> {
760 info!("Proving transaction...");
761
762 let executed_transaction = tx_result.executed_transaction();
763 let proven_transaction = tx_prover.prove(executed_transaction.clone().into()).await?;
764
765 if proven_transaction.id() != executed_transaction.id() {
774 return Err(ClientError::MismatchedProvenTransaction {
775 requested: executed_transaction.id(),
776 returned: proven_transaction.id(),
777 });
778 }
779
780 info!("Transaction proven.");
781
782 Ok(proven_transaction)
783 }
784
785 pub async fn submit_proven_transaction(
795 &mut self,
796 proven_transaction: ProvenTransaction,
797 transaction_inputs: impl Into<TransactionInputs>,
798 ) -> Result<BlockNumber, ClientError> {
799 let account_id = proven_transaction.account_id();
801 if self.is_allowlist_gated(account_id).await? {
802 ensure_account_allowed(account_id, self.is_account_allowed(account_id).await)?;
803 }
804
805 info!("Submitting transaction to the network...");
806 let tx_id = proven_transaction.id();
807 let key = self.transaction_encryption_key().await?;
808
809 let transaction_inputs = transaction_inputs.into();
813
814 let sealed_inputs =
815 seal_transaction_inputs(&mut self.rng, &key, tx_id, &transaction_inputs)?;
816
817 let result =
818 self.rpc_api.submit_proven_transaction(&proven_transaction, sealed_inputs).await;
819 if let Err(err) = &result {
820 self.forget_stale_transaction_encryption_key(err).await;
821 }
822
823 let block_num = result.map_err(|err| {
824 promote_indeterminate_submission(err, proven_transaction, transaction_inputs)
825 })?;
826 info!("Transaction submitted.");
827
828 Ok(block_num)
829 }
830
831 pub(crate) async fn transaction_encryption_key(
839 &self,
840 ) -> Result<TransactionEncryptionKey, ClientError> {
841 if let Some(key) = self.store.get_transaction_encryption_key().await? {
842 return Ok(key);
843 }
844
845 let attested = self.rpc_api.get_transaction_encryption_key().await?;
846
847 let genesis_commitment =
851 self.trusted_block_header(BlockNumber::GENESIS).await?.commitment();
852 let validator_keys = self.get_validator_config().await?;
853
854 let key = attested.verify(genesis_commitment, &validator_keys)?;
855 self.store.set_transaction_encryption_key(&key).await?;
856
857 Ok(key)
858 }
859
860 #[cfg(feature = "testing")]
863 pub async fn seed_transaction_encryption_key(
864 &self,
865 key: TransactionEncryptionKey,
866 ) -> Result<(), ClientError> {
867 Ok(self.store.set_transaction_encryption_key(&key).await?)
868 }
869
870 pub(crate) async fn forget_stale_transaction_encryption_key(&self, err: &RpcError) {
876 if err.is_stale_transaction_encryption_key()
877 && let Err(err) = self.store.remove_transaction_encryption_key().await
878 {
879 tracing::warn!("failed to evict the stale transaction encryption key: {err}");
880 }
881 }
882
883 async fn trusted_block_header(
890 &self,
891 block_num: BlockNumber,
892 ) -> Result<BlockHeader, ClientError> {
893 self.store.get_block_header_by_num(block_num).await?.map(|(header, _)| header).ok_or_else(
894 || {
895 ClientError::ChainValidationError(alloc::format!(
896 "block header {block_num} is not tracked locally; sync the client before it can verify data against the chain"
897 ))
898 },
899 )
900 }
901
902 pub async fn get_transaction_store_update(
905 &self,
906 tx_result: &TransactionResult,
907 submission_height: BlockNumber,
908 ) -> Result<TransactionStoreUpdate, TransactionStoreUpdateError> {
909 let note_updates = self.get_note_updates(submission_height, tx_result).await?;
910
911 let new_tags: Vec<NoteTagRecord> = note_updates
914 .updated_input_notes()
915 .filter_map(|note| {
916 let note = note.inner();
917
918 if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) =
919 note.state()
920 {
921 Some(NoteTagRecord::with_note_source(*tag, note.details_commitment()))
922 } else {
923 None
924 }
925 })
926 .collect();
927
928 Ok(TransactionStoreUpdate::new(
929 tx_result.executed_transaction().clone(),
930 submission_height,
931 note_updates,
932 tx_result.future_notes().to_vec(),
933 new_tags,
934 ))
935 }
936
937 pub async fn apply_transaction(
940 &self,
941 tx_result: &TransactionResult,
942 submission_height: BlockNumber,
943 ) -> Result<(), ClientError> {
944 let tx_update = self.get_transaction_store_update(tx_result, submission_height).await?;
945
946 self.apply_transaction_update(tx_update).await?;
947
948 for observer in &self.transaction_observers {
950 if let Err(err) = observer.apply(tx_result).await {
951 tracing::warn!(
952 observer = observer.name(),
953 error = ?err,
954 "TransactionObserver::apply failed; continuing with remaining observers",
955 );
956 }
957 }
958
959 Ok(())
960 }
961
962 pub async fn apply_transaction_update(
963 &self,
964 tx_update: TransactionStoreUpdate,
965 ) -> Result<(), ClientError> {
966 info!("Applying transaction to the local store...");
969
970 let executed_transaction = tx_update.executed_transaction();
971 let account_id = executed_transaction.account_id();
972
973 if self.account_reader(account_id).status().await?.is_locked() {
974 return Err(ClientError::AccountLocked(account_id));
975 }
976
977 self.store.apply_transaction(tx_update).await?;
978 info!("Transaction stored.");
979 Ok(())
980 }
981
982 pub async fn execute_program(
987 &self,
988 account_id: AccountId,
989 tx_script: TransactionScript,
990 advice_inputs: AdviceInputs,
991 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
992 ) -> Result<[Felt; MIN_STACK_DEPTH], ClientError> {
993 let (data_store, block_ref) =
994 self.prepare_program_execution(account_id, foreign_accounts).await?;
995
996 Ok(self
997 .build_executor(&data_store)?
998 .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
999 .await?)
1000 }
1001
1002 #[cfg(feature = "dap")]
1005 pub async fn execute_program_with_dap(
1006 &self,
1007 account_id: AccountId,
1008 tx_script: TransactionScript,
1009 advice_inputs: AdviceInputs,
1010 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
1011 ) -> Result<[Felt; MIN_STACK_DEPTH], ClientError> {
1012 let (data_store, block_ref) =
1013 self.prepare_program_execution(account_id, foreign_accounts).await?;
1014
1015 Ok(self
1016 .build_dap_executor(&data_store)?
1017 .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
1018 .await?)
1019 }
1020
1021 pub async fn validate_request(
1031 &self,
1032 account_id: AccountId,
1033 transaction_request: &TransactionRequest,
1034 ) -> Result<(), ClientError> {
1035 self.validate_recency().await?;
1036 validate_output_note_senders(transaction_request, account_id)?;
1037 let account: PartialAccount = self
1038 .store
1039 .get_minimal_partial_account(account_id)
1040 .await?
1041 .ok_or(ClientError::AccountDataNotFound(account_id))?
1042 .try_into()?;
1043 self.validate_account_request(transaction_request, account_id, &account.code_interface())
1044 .await
1045 }
1046
1047 async fn validate_account_request(
1052 &self,
1053 transaction_request: &TransactionRequest,
1054 account_id: AccountId,
1055 account_code_interface: &AccountCodeInterface,
1056 ) -> Result<(), ClientError> {
1057 validate_fee_conversion_info_support(transaction_request, account_code_interface)?;
1058
1059 if account_code_interface.contains([FungibleFaucet::mint_and_send_root()]) {
1060 Ok(())
1062 } else {
1063 let assets = self.account_reader(account_id).assets().await?;
1064 validate_basic_account_request(transaction_request, &assets)
1065 }
1066 }
1067
1068 async fn validate_recency(&self) -> Result<(), ClientError> {
1069 if let Some(max_block_number_delta) = self.max_block_number_delta {
1070 let current_chain_tip =
1071 self.rpc_api.get_block_header_by_number(None, false).await?.0.block_num();
1072
1073 if current_chain_tip > self.store.get_sync_height().await? + max_block_number_delta {
1074 return Err(ClientError::RecencyConditionError(
1075 "The client is too far behind the chain tip to execute the transaction",
1076 ));
1077 }
1078 }
1079 Ok(())
1080 }
1081
1082 pub async fn ensure_ntx_scripts_registered(
1095 &mut self,
1096 account_id: AccountId,
1097 scripts: &[NoteScript],
1098 tx_prover: Arc<dyn TransactionProver>,
1099 ) -> Result<(), ClientError> {
1100 let mut missing_scripts = Vec::new();
1101
1102 for script in scripts {
1103 if StandardNote::from_script(script).is_some() {
1105 continue;
1106 }
1107
1108 let script_root = script.root();
1109
1110 match self.rpc_api.get_note_script_by_root(script_root.into()).await {
1112 Ok(Some(_)) => {},
1113 Ok(None) => missing_scripts.push(script.clone()),
1114 Err(source) => {
1115 return Err(ClientError::NtxScriptRegistrationFailed {
1116 script_root: script_root.into(),
1117 source,
1118 });
1119 },
1120 }
1121 }
1122
1123 if missing_scripts.is_empty() {
1124 return Ok(());
1125 }
1126
1127 let registration_request = TransactionRequestBuilder::new().build_register_note_scripts(
1128 account_id,
1129 missing_scripts,
1130 self.rng(),
1131 )?;
1132
1133 let tx_result = self.execute_transaction(account_id, registration_request).await?;
1134 let proven = self.prove_transaction_with(&tx_result, tx_prover).await?;
1135 let submission_height = self.submit_proven_transaction(proven, &tx_result).await?;
1136 self.apply_transaction(&tx_result, submission_height).await?;
1137
1138 Ok(())
1139 }
1140
1141 pub(crate) async fn get_valid_input_notes<STORE: DataStore + Sync>(
1150 &self,
1151 data_store: &STORE,
1152 account_id: AccountId,
1153 block_ref: BlockNumber,
1154 mut input_notes: InputNotes<InputNote>,
1155 tx_args: TransactionArgs,
1156 ) -> Result<InputNotes<InputNote>, ClientError> {
1157 loop {
1158 if input_notes.is_empty() {
1161 break;
1162 }
1163
1164 let execution = NoteConsumptionChecker::new(&self.build_executor(data_store)?)
1165 .check_notes_consumability(
1166 account_id,
1167 block_ref,
1168 input_notes.iter().map(|n| n.clone().into_note()).collect(),
1169 tx_args.clone(),
1170 )
1171 .await?;
1172
1173 if execution.failed().is_empty() {
1174 break;
1175 }
1176
1177 let failed_note_ids: BTreeSet<NoteId> =
1178 execution.failed().iter().map(|n| n.note().id()).collect();
1179 let filtered_input_notes = InputNotes::new(
1180 input_notes
1181 .into_iter()
1182 .filter(|note| !failed_note_ids.contains(¬e.id()))
1183 .collect(),
1184 )
1185 .expect("Created from a valid input notes list");
1186
1187 input_notes = filtered_input_notes;
1188 }
1189
1190 Ok(input_notes)
1191 }
1192
1193 pub async fn get_foreign_account_inputs(
1215 &self,
1216 foreign_accounts: impl IntoIterator<Item = ForeignAccount>,
1217 block_num: BlockNumber,
1218 ) -> Result<Vec<AccountInputs>, ClientError> {
1219 let foreign_accounts = foreign_accounts.into_iter();
1220 let mut return_foreign_account_inputs = Vec::with_capacity(foreign_accounts.size_hint().0);
1221
1222 for foreign_account in foreign_accounts {
1223 let foreign_account_inputs = match foreign_account {
1224 ForeignAccount::Public(account_id, storage_requirements) => {
1225 fetch_public_account_inputs(
1226 &self.store,
1227 &self.rpc_api,
1228 account_id,
1229 storage_requirements,
1230 AccountStateAt::Block(block_num),
1231 )
1232 .await?
1233 },
1234 ForeignAccount::Private(partial_account) => {
1235 let witness =
1238 self.get_account_witness_at(partial_account.id(), block_num).await?;
1239
1240 AccountInputs::new(partial_account, witness)
1241 },
1242 ForeignAccount::Prefetched(inputs) => inputs,
1243 };
1244
1245 return_foreign_account_inputs.push(foreign_account_inputs);
1246 }
1247
1248 Ok(return_foreign_account_inputs)
1249 }
1250
1251 async fn get_account_witness_at(
1258 &self,
1259 account_id: AccountId,
1260 block_num: BlockNumber,
1261 ) -> Result<AccountWitness, ClientError> {
1262 if block_num == self.store.get_sync_height().await?
1264 && let Some(witness) = self.store.get_account_witness(account_id).await?
1265 {
1266 return Ok(witness);
1267 }
1268
1269 let (_, account_proof) = self
1270 .rpc_api
1271 .get_account(account_id, GetAccountRequest::new().at(AccountStateAt::Block(block_num)))
1272 .await?;
1273
1274 Ok(account_proof.into_parts().0)
1275 }
1276
1277 async fn prepare_program_execution(
1281 &self,
1282 account_id: AccountId,
1283 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
1284 ) -> Result<(ClientDataStore, BlockNumber), ClientError> {
1285 let block_ref = self.get_sync_height().await?;
1286
1287 let foreign_account_inputs = self
1288 .get_foreign_account_inputs(foreign_accounts.into_values(), block_ref)
1289 .await?;
1290
1291 let account_code = self
1292 .store
1293 .get_account_code(account_id)
1294 .await?
1295 .ok_or(ClientError::AccountDataNotFound(account_id))?;
1296
1297 let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
1298
1299 data_store.mast_store().load_account_code(&account_code);
1301
1302 for fpi_account in &foreign_account_inputs {
1303 data_store.mast_store().load_account_code(fpi_account.code());
1304 }
1305
1306 data_store.register_foreign_account_inputs(foreign_account_inputs);
1307
1308 Ok((data_store, block_ref))
1309 }
1310
1311 pub(crate) fn build_executor<'store, 'auth, STORE: DataStore + Sync>(
1314 &'auth self,
1315 data_store: &'store STORE,
1316 ) -> Result<TransactionExecutor<'store, 'auth, STORE, AUTH>, TransactionExecutorError> {
1317 let mut executor = TransactionExecutor::new(data_store)
1318 .with_options(self.exec_options)?
1319 .with_source_manager(self.source_manager.clone());
1320 if let Some(authenticator) = self.authenticator.as_deref() {
1321 executor = executor.with_authenticator(authenticator);
1322 }
1323 Ok(executor)
1324 }
1325
1326 async fn get_native_account_record(
1331 &self,
1332 account_id: AccountId,
1333 ) -> Result<AccountRecord, ClientError> {
1334 let account_record = self
1335 .store
1336 .get_minimal_partial_account(account_id)
1337 .await?
1338 .ok_or(ClientError::AccountDataNotFound(account_id))?;
1339 if account_record.is_watched() {
1340 return Err(ClientError::AccountIsWatched(account_id));
1341 }
1342 Ok(account_record)
1343 }
1344
1345 #[cfg(feature = "dap")]
1347 pub(crate) fn build_dap_executor<'store, 'auth, STORE: DataStore + Sync>(
1348 &'auth self,
1349 data_store: &'store STORE,
1350 ) -> Result<
1351 TransactionExecutor<'store, 'auth, STORE, AUTH, dap_executor::DapProgramExecutor>,
1352 TransactionExecutorError,
1353 > {
1354 Ok(self
1355 .build_executor(data_store)?
1356 .with_program_executor::<dap_executor::DapProgramExecutor>())
1357 }
1358
1359 async fn get_note_updates(
1362 &self,
1363 submission_height: BlockNumber,
1364 tx_result: &TransactionResult,
1365 ) -> Result<NoteUpdateTracker, TransactionStoreUpdateError> {
1366 let executed_tx = tx_result.executed_transaction();
1367 let current_timestamp = self.store.get_current_timestamp();
1368 let current_block_num = self.store.get_sync_height().await?;
1369
1370 let new_output_notes = executed_tx
1382 .output_notes()
1383 .iter()
1384 .filter(|output_note| {
1385 output_note
1386 .recipient()
1387 .is_none_or(|recipient| recipient.script().root() != TxFeeNote::script_root())
1388 })
1389 .cloned()
1390 .filter_map(|output_note| {
1391 OutputNoteRecord::try_from_output_note(output_note, submission_height).ok()
1392 })
1393 .collect::<Vec<_>>();
1394
1395 let mut new_input_notes = vec![];
1397 let output_notes: Vec<Note> =
1398 notes_from_output(executed_tx.output_notes()).cloned().collect();
1399 let note_screener = self.note_screener().clone();
1400 let output_note_relevances = note_screener.get_batch_consumability(&output_notes).await?;
1401
1402 for note in output_notes {
1403 if note.script().root() == TxFeeNote::script_root() {
1408 continue;
1409 }
1410
1411 if output_note_relevances.contains_key(¬e.id()) {
1412 let metadata = *note.metadata();
1413 let tag = metadata.tag();
1414 let attachments = note.attachments().clone();
1415
1416 new_input_notes.push(InputNoteRecord::new(
1417 note.into(),
1418 attachments,
1419 current_timestamp,
1420 ExpectedNoteState {
1421 metadata: Some(metadata),
1422 after_block_num: submission_height,
1423 tag: Some(tag),
1424 }
1425 .into(),
1426 ));
1427 }
1428 }
1429
1430 new_input_notes.extend(tx_result.future_notes().iter().map(|(note_details, tag)| {
1432 InputNoteRecord::new(
1433 note_details.clone(),
1434 NoteAttachments::empty(),
1435 None,
1436 ExpectedNoteState {
1437 metadata: None,
1438 after_block_num: current_block_num,
1439 tag: Some(*tag),
1440 }
1441 .into(),
1442 )
1443 }));
1444
1445 let consumed_note_ids =
1450 executed_tx.tx_inputs().input_notes().iter().map(InputNote::id).collect();
1451
1452 let consumed_notes =
1453 self.store.get_input_notes(NoteFilter::List(consumed_note_ids)).await?;
1454
1455 let tracked_note_ids =
1456 consumed_notes.iter().filter_map(InputNoteRecord::id).collect::<BTreeSet<_>>();
1457
1458 for input_note in executed_tx.tx_inputs().input_notes() {
1459 if !tracked_note_ids.contains(&input_note.id()) {
1460 let mut input_note_record = InputNoteRecord::from(input_note.clone());
1461 input_note_record.consumed_locally(
1462 executed_tx.account_id(),
1463 executed_tx.id(),
1464 current_timestamp,
1465 )?;
1466 new_input_notes.push(input_note_record);
1467 }
1468 }
1469
1470 let mut updated_input_notes = vec![];
1471
1472 for mut input_note_record in consumed_notes {
1473 if input_note_record.consumed_locally(
1474 executed_tx.account_id(),
1475 executed_tx.id(),
1476 current_timestamp,
1477 )? {
1478 updated_input_notes.push(input_note_record);
1479 }
1480 }
1481
1482 Ok(NoteUpdateTracker::for_transaction_updates(
1483 new_input_notes,
1484 updated_input_notes,
1485 new_output_notes,
1486 ))
1487 }
1488}
1489
1490#[derive(Debug, thiserror::Error)]
1496pub enum TransactionStoreUpdateError {
1497 #[error("store error")]
1498 Store(#[from] StoreError),
1499 #[error("note screener error")]
1500 NoteScreener(#[from] NoteScreenerError),
1501 #[error("note record error")]
1502 NoteRecord(#[from] NoteRecordError),
1503}
1504
1505#[derive(Clone, Copy, Debug)]
1509enum TransactionExecutionMode {
1510 Standard,
1511 #[cfg(feature = "dap")]
1512 Dap,
1513}
1514
1515pub(crate) struct PreparedTransaction {
1517 pub(crate) notes: InputNotes<InputNote>,
1518 pub(crate) output_recipients: Vec<NoteRecipient>,
1519 pub(crate) future_notes: Vec<(NoteDetails, NoteTag)>,
1520 pub(crate) tx_args: TransactionArgs,
1521 pub(crate) foreign_account_inputs: Vec<AccountInputs>,
1522 pub(crate) block_numbers: BTreeSet<BlockNumber>,
1523 pub(crate) block_num: BlockNumber,
1524 pub(crate) ignore_invalid_notes: bool,
1525}
1526
1527impl PreparedTransaction {
1528 pub(crate) fn output_note_scripts(&self) -> impl Iterator<Item = NoteScript> + '_ {
1531 self.output_recipients.iter().map(|recipient| recipient.script().clone())
1532 }
1533}
1534
1535fn get_outgoing_assets(
1540 transaction_request: &TransactionRequest,
1541) -> (BTreeMap<AccountId, u64>, Vec<Asset>) {
1542 let mut own_notes_assets = match transaction_request.script_template() {
1543 Some(TransactionScriptTemplate::SendNotes(notes)) => notes
1544 .iter()
1545 .map(|note| (note.id(), note.assets().clone()))
1546 .collect::<BTreeMap<_, _>>(),
1547 _ => BTreeMap::default(),
1548 };
1549 let mut output_notes_assets = transaction_request
1550 .expected_output_own_notes()
1551 .into_iter()
1552 .map(|note| (note.id(), note.assets().clone()))
1553 .collect::<BTreeMap<_, _>>();
1554
1555 output_notes_assets.append(&mut own_notes_assets);
1557
1558 let outgoing_assets = output_notes_assets.values().flat_map(|note_assets| note_assets.iter());
1560
1561 request::collect_assets(outgoing_assets)
1562}
1563
1564fn attach_native_fee_conversion_info(
1579 transaction_request: &mut TransactionRequest,
1580 account_code_interface: &AccountCodeInterface,
1581 reference_header: &BlockHeader,
1582 protocol_config: &ProtocolConfig,
1583) -> Result<(), ClientError> {
1584 if transaction_request.has_auth_arg() {
1588 return Ok(());
1589 }
1590
1591 let fee_parameters = reference_header.fee_parameters();
1592 let declared_salt = transaction_request.fee_conversion_salt();
1593 if fee_parameters.verification_base_fee() == 0 && declared_salt.is_none() {
1594 return Ok(());
1595 }
1596
1597 match FeeAuth::of(account_code_interface) {
1598 FeeAuth::FixedSalt => {
1599 transaction_request.commit_native_fee_conversion_info(
1600 protocol_config.fee_asset_id().faucet_id(),
1601 declared_salt.unwrap_or(NATIVE_FEE_CONVERSION_SALT),
1602 );
1603 Ok(())
1604 },
1605 FeeAuth::CallerChosenSalt(component) => match declared_salt {
1606 Some(salt) => {
1607 transaction_request.commit_native_fee_conversion_info(
1608 protocol_config.fee_asset_id().faucet_id(),
1609 salt,
1610 );
1611 Ok(())
1612 },
1613 None => Err(ClientError::TransactionRequestError(
1614 TransactionRequestError::FeeConversionInfoRequired(component),
1615 )),
1616 },
1617 FeeAuth::Ignored(component) => match declared_salt {
1618 Some(_) => Err(ClientError::TransactionRequestError(
1621 TransactionRequestError::FeeConversionInfoUnsupported(component),
1622 )),
1623 None => Ok(()),
1624 },
1625 }
1626}
1627
1628enum FeeAuth {
1630 FixedSalt,
1633 CallerChosenSalt(String),
1636 Ignored(String),
1640}
1641
1642impl FeeAuth {
1643 fn of(account_code_interface: &AccountCodeInterface) -> Self {
1652 let procedures: Vec<_> = account_code_interface.procedures().iter().copied().collect();
1653 let components = AccountComponentInterface::from_procedures(&procedures);
1654
1655 if components
1656 .iter()
1657 .any(|component| matches!(component, AccountComponentInterface::AuthSingleSig))
1658 {
1659 return Self::FixedSalt;
1660 }
1661
1662 let caller_chosen_salt = components.iter().find_map(|component| match component {
1667 AccountComponentInterface::AuthMultisig
1668 | AccountComponentInterface::AuthMultisigSmart
1669 | AccountComponentInterface::AuthGuardedMultisig => {
1670 Some(Self::CallerChosenSalt(component.name()))
1671 },
1672 _ => None,
1673 });
1674
1675 caller_chosen_salt.unwrap_or_else(|| {
1676 let name = components
1677 .iter()
1678 .find(|component| {
1679 matches!(
1680 component,
1681 AccountComponentInterface::AuthNoAuth
1682 | AccountComponentInterface::AuthNetworkAccount
1683 )
1684 })
1685 .map_or_else(|| "unrecognized".into(), AccountComponentInterface::name);
1686
1687 Self::Ignored(name)
1688 })
1689 }
1690}
1691
1692pub(crate) fn native_fee_conversion_info(
1697 account_code_interface: &AccountCodeInterface,
1698 fee_parameters: &FeeParameters,
1699 protocol_config: &ProtocolConfig,
1700) -> Option<FeeConversionInfo> {
1701 if fee_parameters.verification_base_fee() == 0 {
1702 return None;
1703 }
1704
1705 match FeeAuth::of(account_code_interface) {
1708 FeeAuth::FixedSalt => {
1709 Some(FeeConversionInfo::one_to_one(protocol_config.fee_asset_id().faucet_id()))
1710 },
1711 FeeAuth::CallerChosenSalt(_) | FeeAuth::Ignored(_) => None,
1712 }
1713}
1714
1715fn validate_fee_conversion_info_support(
1721 transaction_request: &TransactionRequest,
1722 account_code_interface: &AccountCodeInterface,
1723) -> Result<(), ClientError> {
1724 if transaction_request.fee_conversion_salt().is_none() {
1725 return Ok(());
1726 }
1727
1728 match FeeAuth::of(account_code_interface) {
1729 FeeAuth::FixedSalt | FeeAuth::CallerChosenSalt(_) => Ok(()),
1730 FeeAuth::Ignored(auth_component) => Err(ClientError::TransactionRequestError(
1731 TransactionRequestError::FeeConversionInfoUnsupported(auth_component),
1732 )),
1733 }
1734}
1735fn validate_output_note_senders(
1743 transaction_request: &TransactionRequest,
1744 account_id: AccountId,
1745) -> Result<(), ClientError> {
1746 for note in transaction_request.expected_output_own_notes() {
1747 let sender = note.metadata().sender();
1748 if sender != account_id {
1749 return Err(ClientError::TransactionRequestError(
1750 TransactionRequestError::OutputNoteSenderMismatch {
1751 expected: account_id,
1752 actual: sender,
1753 },
1754 ));
1755 }
1756 }
1757
1758 Ok(())
1759}
1760
1761fn validate_basic_account_request(
1764 transaction_request: &TransactionRequest,
1765 vault_assets: &[Asset],
1766) -> Result<(), ClientError> {
1767 let (fungible_balance_map, non_fungible_set) = get_outgoing_assets(transaction_request);
1768 let (incoming_fungible_balance_map, incoming_non_fungible_balance_set) =
1769 transaction_request.incoming_assets();
1770
1771 let mut available_fungible: BTreeMap<AccountId, u64> = BTreeMap::new();
1774 for asset in vault_assets {
1775 if let Some(fungible) = asset.as_fungible() {
1776 let balance = available_fungible.entry(fungible.faucet_id()).or_default();
1777 *balance = balance.saturating_add(fungible.amount().as_u64());
1778 }
1779 }
1780
1781 for (faucet_id, amount) in fungible_balance_map {
1784 let account_asset_amount = available_fungible.get(&faucet_id).copied().unwrap_or(0);
1785 let incoming_balance = incoming_fungible_balance_map.get(&faucet_id).unwrap_or(&0);
1786 if account_asset_amount + incoming_balance < amount {
1787 return Err(ClientError::AssetError(AssetError::FungibleAssetAmountNotSufficient {
1788 minuend: account_asset_amount,
1789 subtrahend: amount,
1790 }));
1791 }
1792 }
1793
1794 for non_fungible in &non_fungible_set {
1797 let held = vault_assets.iter().any(|asset| asset == non_fungible);
1798 if !held && !incoming_non_fungible_balance_set.contains(non_fungible) {
1799 return Err(ClientError::TransactionRequestError(
1800 TransactionRequestError::MissingNonFungibleAsset(non_fungible.faucet_id()),
1801 ));
1802 }
1803 }
1804
1805 Ok(())
1806}
1807
1808async fn local_account_inputs(
1818 store: &Arc<dyn Store>,
1819 account_id: AccountId,
1820 account_state_at: AccountStateAt,
1821) -> Result<Option<AccountInputs>, ClientError> {
1822 if let AccountStateAt::Block(block_num) = account_state_at
1824 && block_num == store.get_sync_height().await?
1825 && let Some(witness) = store.get_account_witness(account_id).await?
1826 && let Some(record) = store.get_minimal_partial_account(account_id).await?
1827 {
1828 let account: PartialAccount = record.try_into()?;
1831 if account.to_commitment() == witness.state_commitment() {
1832 return Ok(Some(AccountInputs::new(account, witness)));
1833 }
1834 }
1835
1836 Ok(None)
1837}
1838
1839pub(crate) async fn fetch_public_account_inputs(
1850 store: &Arc<dyn Store>,
1851 rpc_api: &Arc<dyn NodeRpcClient>,
1852 account_id: AccountId,
1853 storage_requirements: AccountStorageRequirements,
1854 account_state_at: AccountStateAt,
1855) -> Result<AccountInputs, ClientError> {
1856 if let Some(inputs) = local_account_inputs(store, account_id, account_state_at).await? {
1857 return Ok(inputs);
1858 }
1859
1860 let known_code: Option<AccountCode> =
1861 store.get_foreign_account_code(vec![account_id]).await?.into_values().next();
1862
1863 let vault = store
1866 .get_account_header(account_id)
1867 .await?
1868 .map_or(VaultFetch::Always, |(header, ..)| {
1869 VaultFetch::IfChangedFrom(header.vault_root())
1870 });
1871
1872 let (_block_num, account_proof) = rpc_api
1873 .get_account(
1874 account_id,
1875 GetAccountRequest::new()
1876 .with_storage(StorageMapFetch::Slots(storage_requirements.clone()))
1877 .at(account_state_at)
1878 .with_known_code(known_code)
1879 .with_vault(vault),
1880 )
1881 .await?;
1882
1883 let account_inputs = request::account_proof_into_inputs(account_proof)?;
1884
1885 let _ = store
1886 .upsert_foreign_account_code(account_id, account_inputs.code().clone())
1887 .await
1888 .inspect_err(|err| {
1889 tracing::warn!(
1890 %account_id,
1891 %err,
1892 "Failed to persist foreign account code to store"
1893 );
1894 });
1895
1896 Ok(account_inputs)
1897}
1898
1899fn promote_indeterminate_submission(
1902 err: RpcError,
1903 transaction: ProvenTransaction,
1904 transaction_inputs: TransactionInputs,
1905) -> ClientError {
1906 if !err.is_indeterminate_submission() {
1907 return ClientError::RpcError(err);
1908 }
1909
1910 ClientError::SubmissionOutcomeUnknown {
1911 transaction: Box::new(transaction),
1912 transaction_inputs: Box::new(transaction_inputs),
1913 source: err,
1914 }
1915}
1916
1917pub fn notes_from_output(output_notes: &RawOutputNotes) -> impl Iterator<Item = &Note> {
1922 output_notes.iter().filter_map(|n| match n {
1923 RawOutputNote::Full(n) => Some(n),
1924 RawOutputNote::Partial(_) => None,
1925 })
1926}
1927
1928pub(crate) fn validate_executed_transaction(
1931 executed_transaction: &ExecutedTransaction,
1932 expected_output_recipients: &[NoteRecipient],
1933) -> Result<(), ClientError> {
1934 let tx_output_recipient_digests = executed_transaction
1935 .output_notes()
1936 .iter()
1937 .filter_map(|n| n.recipient().map(NoteRecipient::digest))
1938 .collect::<Vec<_>>();
1939
1940 let missing_recipient_digest: Vec<Word> = expected_output_recipients
1941 .iter()
1942 .filter_map(|recipient| {
1943 (!tx_output_recipient_digests.contains(&recipient.digest()))
1944 .then_some(recipient.digest())
1945 })
1946 .collect();
1947
1948 if !missing_recipient_digest.is_empty() {
1949 return Err(ClientError::MissingOutputRecipients(missing_recipient_digest));
1950 }
1951
1952 Ok(())
1953}
1954
1955fn ensure_account_allowed(
1960 account_id: AccountId,
1961 is_allowed: Result<bool, ClientError>,
1962) -> Result<(), ClientError> {
1963 match is_allowed {
1964 Ok(true) => Ok(()),
1965 Ok(false) => Err(ClientError::AccountNotAllowlisted(account_id)),
1966 Err(err) => {
1967 info!(
1968 "could not check whether account {account_id} is on the network allowlist, \
1969 submitting anyway and letting the node decide: {err}"
1970 );
1971 Ok(())
1972 },
1973 }
1974}
1975
1976#[cfg(test)]
1980mod tests {
1981 use alloc::vec;
1982
1983 use miden_protocol::Word;
1984 use miden_protocol::account::auth::AuthSecretKey;
1985 use miden_protocol::account::{
1986 Account,
1987 AccountBuilder,
1988 AccountComponent,
1989 AccountComponentMetadata,
1990 AccountId,
1991 AccountType,
1992 };
1993 use miden_protocol::asset::{AssetId, FungibleAsset};
1994 use miden_protocol::block::{BlockHeader, BlockNumber, FeeParameters};
1995 use miden_protocol::note::{Note, NoteType};
1996 use miden_protocol::protocol_config::ProtocolConfig;
1997 use miden_protocol::testing::account_id::{
1998 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
1999 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
2000 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
2001 ACCOUNT_ID_SENDER,
2002 };
2003 use miden_standards::account::AccountBuilderSchemaCommitmentExt;
2004 use miden_standards::account::auth::{
2005 Approver,
2006 ApproverSet,
2007 AuthGuardedMultisig,
2008 AuthGuardedMultisigConfig,
2009 AuthMultisig,
2010 AuthMultisigConfig,
2011 AuthMultisigSmart,
2012 AuthMultisigSmartConfig,
2013 AuthSingleSig,
2014 FeeConversionInfo,
2015 GuardianConfig,
2016 NoAuth,
2017 commit_fee_conversion_info,
2018 };
2019 use miden_standards::account::wallets::BasicWallet;
2020 use miden_standards::note::P2idNote;
2021 use rand::SeedableRng;
2022 use rand_chacha::ChaCha20Rng;
2023
2024 use super::{
2025 AccountComponentInterface,
2026 NATIVE_FEE_CONVERSION_SALT,
2027 TransactionRequest,
2028 TransactionRequestBuilder,
2029 attach_native_fee_conversion_info,
2030 validate_fee_conversion_info_support,
2031 validate_output_note_senders,
2032 };
2033 use crate::ClientError;
2034 use crate::assembly::CodeBuilder;
2035 use crate::auth::AuthSchemeId;
2036 use crate::rng::draw_word;
2037 use crate::transaction::TransactionRequestError;
2038
2039 fn own_note_with_sender(sender: AccountId) -> Note {
2040 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
2041 let target_id =
2042 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
2043 let mut rng = ChaCha20Rng::seed_from_u64(0);
2044
2045 P2idNote::builder()
2046 .sender(sender)
2047 .target(target_id)
2048 .asset(FungibleAsset::new(faucet_id, 100).unwrap())
2049 .note_type(NoteType::Public)
2050 .serial_number(draw_word(&mut rng))
2051 .build()
2052 .expect("note creation failed")
2053 .into()
2054 }
2055
2056 #[test]
2057 fn output_note_with_foreign_sender_is_rejected() {
2058 let account_id =
2059 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
2060 let foreign_sender = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
2061 assert_ne!(account_id, foreign_sender);
2062
2063 let request = TransactionRequestBuilder::new()
2064 .own_output_notes(vec![own_note_with_sender(foreign_sender)])
2065 .build()
2066 .unwrap();
2067
2068 let err = validate_output_note_senders(&request, account_id).unwrap_err();
2069 match err {
2070 ClientError::TransactionRequestError(
2071 TransactionRequestError::OutputNoteSenderMismatch { expected, actual },
2072 ) => {
2073 assert_eq!(expected, account_id);
2074 assert_eq!(actual, foreign_sender);
2075 },
2076 other => panic!("expected OutputNoteSenderMismatch, got {other:?}"),
2077 }
2078 }
2079
2080 #[test]
2081 fn output_note_with_matching_sender_is_accepted() {
2082 let account_id =
2083 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
2084
2085 let request = TransactionRequestBuilder::new()
2086 .own_output_notes(vec![own_note_with_sender(account_id)])
2087 .build()
2088 .unwrap();
2089
2090 validate_output_note_senders(&request, account_id).unwrap();
2091 }
2092
2093 #[test]
2094 fn request_without_own_output_notes_is_accepted() {
2095 let account_id =
2096 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
2097 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
2098
2099 let request = TransactionRequestBuilder::new()
2101 .input_notes(vec![(own_note_with_sender(faucet_id), None)])
2102 .build()
2103 .unwrap();
2104
2105 validate_output_note_senders(&request, account_id).unwrap();
2106 }
2107
2108 fn account_with_auth(auth_component: impl Into<AccountComponent>) -> Account {
2110 AccountBuilder::new([7u8; 32])
2111 .account_type(AccountType::Public)
2112 .with_component(auth_component)
2113 .with_component(BasicWallet)
2114 .build_with_schema_commitment()
2115 .expect("account creation failed")
2116 }
2117
2118 fn fee_conversion_request() -> TransactionRequest {
2119 TransactionRequestBuilder::new()
2120 .fee_conversion_salt(Word::from([13u32, 14, 15, 16]))
2121 .build()
2122 .unwrap()
2123 }
2124
2125 #[test]
2126 fn fee_conversion_info_is_accepted_by_a_signature_authenticated_account() {
2127 let key = AuthSecretKey::new_falcon512_poseidon2();
2128 let auth = AuthSingleSig::new(Approver::new(
2129 key.public_key().to_commitment(),
2130 AuthSchemeId::Falcon512Poseidon2,
2131 ));
2132
2133 validate_fee_conversion_info_support(
2134 &fee_conversion_request(),
2135 &account_with_auth(auth).code_interface(),
2136 )
2137 .unwrap();
2138 }
2139
2140 #[test]
2141 fn fee_conversion_info_is_rejected_by_an_account_that_cannot_read_it() {
2142 let account = account_with_auth(NoAuth);
2143
2144 let err = validate_fee_conversion_info_support(
2145 &fee_conversion_request(),
2146 &account.code_interface(),
2147 )
2148 .expect_err("NoAuth does not read the auth args");
2149 match err {
2150 ClientError::TransactionRequestError(
2151 TransactionRequestError::FeeConversionInfoUnsupported(auth_component),
2152 ) => assert_eq!(auth_component, AccountComponentInterface::AuthNoAuth.name()),
2153 other => panic!("expected FeeConversionInfoUnsupported, got {other:?}"),
2154 }
2155 }
2156
2157 #[test]
2158 fn a_request_without_fee_conversion_info_skips_the_auth_component_check() {
2159 validate_fee_conversion_info_support(
2161 &TransactionRequestBuilder::new().build().unwrap(),
2162 &account_with_auth(NoAuth).code_interface(),
2163 )
2164 .unwrap();
2165 }
2166
2167 const NATIVE_FEE_FAUCET: u128 = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET;
2173
2174 fn test_protocol_config() -> ProtocolConfig {
2175 ProtocolConfig::current(AssetId::new_fungible(NATIVE_FEE_FAUCET.try_into().unwrap()))
2176 .unwrap()
2177 }
2178
2179 fn header_with_base_fee(verification_base_fee: u32) -> BlockHeader {
2181 let fee_parameters = FeeParameters::new(verification_base_fee);
2182 let (_, validator_keys) = miden_protocol::block::ValidatorConfig::random_with_signers(1);
2183
2184 BlockHeader::new(
2185 Word::empty(),
2186 BlockNumber::from(1u32),
2187 Word::empty(),
2188 Word::empty(),
2189 Word::empty(),
2190 Word::empty(),
2191 Word::empty(),
2192 validator_keys,
2193 fee_parameters,
2194 test_protocol_config().to_commitment(),
2195 None,
2196 0,
2197 )
2198 }
2199
2200 fn injected_auth_arg(
2203 mut request: TransactionRequest,
2204 account: &Account,
2205 verification_base_fee: u32,
2206 ) -> Option<Word> {
2207 let _ = attach_native_fee_conversion_info(
2208 &mut request,
2209 &account.code_interface(),
2210 &header_with_base_fee(verification_base_fee),
2211 &test_protocol_config(),
2212 );
2213 *request.auth_arg()
2214 }
2215
2216 fn try_injected_auth_arg(
2218 mut request: TransactionRequest,
2219 account: &Account,
2220 verification_base_fee: u32,
2221 ) -> Result<Option<Word>, ClientError> {
2222 attach_native_fee_conversion_info(
2223 &mut request,
2224 &account.code_interface(),
2225 &header_with_base_fee(verification_base_fee),
2226 &test_protocol_config(),
2227 )?;
2228 Ok(*request.auth_arg())
2229 }
2230
2231 fn singlesig_account() -> Account {
2232 let key = AuthSecretKey::new_falcon512_poseidon2();
2233 account_with_auth(AuthSingleSig::new(Approver::new(
2234 key.public_key().to_commitment(),
2235 AuthSchemeId::Falcon512Poseidon2,
2236 )))
2237 }
2238
2239 fn guarded_multisig_account() -> Account {
2240 let approvers = ApproverSet::new(
2241 vec![Approver::new(
2242 AuthSecretKey::new_falcon512_poseidon2().public_key().to_commitment(),
2243 AuthSchemeId::Falcon512Poseidon2,
2244 )],
2245 1,
2246 )
2247 .unwrap();
2248
2249 let guardian = GuardianConfig::new(Approver::new(
2250 AuthSecretKey::new_falcon512_poseidon2().public_key().to_commitment(),
2251 AuthSchemeId::Falcon512Poseidon2,
2252 ));
2253
2254 account_with_auth(
2255 AuthGuardedMultisig::new(AuthGuardedMultisigConfig::new(approvers, guardian).unwrap())
2256 .unwrap(),
2257 )
2258 }
2259
2260 #[test]
2261 fn native_fee_conversion_info_is_attached_on_a_fee_charging_chain() {
2262 let auth_arg = injected_auth_arg(
2263 TransactionRequestBuilder::new().build().unwrap(),
2264 &singlesig_account(),
2265 500,
2266 )
2267 .expect("a fee-charging chain should get conversion info attached");
2268
2269 let (expected, _) = commit_fee_conversion_info(
2270 FeeConversionInfo::one_to_one(AccountId::try_from(NATIVE_FEE_FAUCET).unwrap()),
2271 NATIVE_FEE_CONVERSION_SALT,
2272 );
2273 assert_eq!(auth_arg, expected, "the fee should be paid in the native asset at rate 1/1");
2274 }
2275
2276 #[test]
2277 fn an_explicit_auth_arg_is_not_overwritten() {
2278 let auth_arg = Word::from([21u32, 22, 23, 24]);
2279 let request = TransactionRequestBuilder::new().auth_arg(auth_arg).build().unwrap();
2280
2281 assert_eq!(
2282 injected_auth_arg(request, &singlesig_account(), 500),
2283 Some(auth_arg),
2284 "a request that declares its own auth arg keeps it"
2285 );
2286 }
2287
2288 fn native_commitment(salt: Word) -> Word {
2290 let (auth_arg, _) = commit_fee_conversion_info(
2291 FeeConversionInfo::one_to_one(AccountId::try_from(NATIVE_FEE_FAUCET).unwrap()),
2292 salt,
2293 );
2294 auth_arg
2295 }
2296
2297 #[test]
2298 fn a_declared_salt_is_used_for_the_native_commitment() {
2299 let salt = Word::from([17u32, 18, 19, 20]);
2300 let request = TransactionRequestBuilder::new().fee_conversion_salt(salt).build().unwrap();
2301
2302 let auth_arg = injected_auth_arg(request, &singlesig_account(), 500)
2303 .expect("a declared salt should still get native conversion info attached");
2304
2305 assert_eq!(auth_arg, native_commitment(salt));
2306 }
2307
2308 fn multisig_account() -> Account {
2309 let approvers = ApproverSet::new(
2310 vec![Approver::new(
2311 AuthSecretKey::new_falcon512_poseidon2().public_key().to_commitment(),
2312 AuthSchemeId::Falcon512Poseidon2,
2313 )],
2314 1,
2315 )
2316 .unwrap();
2317
2318 account_with_auth(AuthMultisig::new(AuthMultisigConfig::new(approvers)).unwrap())
2319 }
2320
2321 #[test]
2322 fn nothing_is_attached_where_it_is_not_needed_or_not_readable() {
2323 for (case, account, base_fee) in [
2324 ("a zero base fee charges nothing", singlesig_account(), 0),
2325 ("NoAuth never reads the auth args", account_with_auth(NoAuth), 500),
2326 ("a multisig salt is its own replay guard", multisig_account(), 500),
2327 ] {
2328 assert_eq!(
2329 injected_auth_arg(
2330 TransactionRequestBuilder::new().build().unwrap(),
2331 &account,
2332 base_fee
2333 ),
2334 None,
2335 "{case}"
2336 );
2337 }
2338 }
2339
2340 #[test]
2347 fn fee_conversion_info_is_accepted_by_a_guarded_multisig_account() {
2348 validate_fee_conversion_info_support(
2349 &fee_conversion_request(),
2350 &guarded_multisig_account().code_interface(),
2351 )
2352 .expect("a guarded multisig reads the auth args as conversion info");
2353 }
2354
2355 #[test]
2359 fn a_guarded_multisig_account_must_declare_its_own_fee_conversion_info() {
2360 let err = try_injected_auth_arg(
2361 TransactionRequestBuilder::new().build().unwrap(),
2362 &guarded_multisig_account(),
2363 500,
2364 )
2365 .expect_err("a guarded multisig account cannot inherit the fixed native salt");
2366 match err {
2367 ClientError::TransactionRequestError(
2368 TransactionRequestError::FeeConversionInfoRequired(auth_component),
2369 ) => {
2370 assert_eq!(auth_component, AccountComponentInterface::AuthGuardedMultisig.name());
2371 },
2372 other => panic!("expected FeeConversionInfoRequired, got {other:?}"),
2373 }
2374
2375 let salt = Word::from([13u32, 14, 15, 16]);
2376 assert_eq!(
2377 try_injected_auth_arg(fee_conversion_request(), &guarded_multisig_account(), 500)
2378 .expect("a declared salt is accepted"),
2379 Some(native_commitment(salt)),
2380 "a guarded multisig account that declares a salt commits the native conversion info"
2381 );
2382
2383 assert_eq!(
2384 try_injected_auth_arg(
2385 TransactionRequestBuilder::new().build().unwrap(),
2386 &guarded_multisig_account(),
2387 0
2388 )
2389 .expect("a chain charging nothing needs no conversion info"),
2390 None,
2391 );
2392 }
2393
2394 fn smart_multisig_account() -> Account {
2398 let approvers = ApproverSet::new(
2399 vec![Approver::new(
2400 AuthSecretKey::new_falcon512_poseidon2().public_key().to_commitment(),
2401 AuthSchemeId::Falcon512Poseidon2,
2402 )],
2403 1,
2404 )
2405 .unwrap();
2406
2407 account_with_auth(AuthMultisigSmart::new(AuthMultisigSmartConfig::new(approvers)).unwrap())
2408 }
2409
2410 #[test]
2415 fn fee_conversion_info_is_accepted_by_a_smart_multisig_account() {
2416 validate_fee_conversion_info_support(
2417 &fee_conversion_request(),
2418 &smart_multisig_account().code_interface(),
2419 )
2420 .expect("a smart multisig reads the auth args as conversion info");
2421 }
2422
2423 #[test]
2427 fn a_smart_multisig_account_must_declare_its_own_fee_conversion_info() {
2428 let err = try_injected_auth_arg(
2429 TransactionRequestBuilder::new().build().unwrap(),
2430 &smart_multisig_account(),
2431 500,
2432 )
2433 .expect_err("a smart multisig account cannot inherit the fixed native salt");
2434 match err {
2435 ClientError::TransactionRequestError(
2436 TransactionRequestError::FeeConversionInfoRequired(auth_component),
2437 ) => {
2438 assert_eq!(auth_component, AccountComponentInterface::AuthMultisigSmart.name());
2439 },
2440 other => panic!("expected FeeConversionInfoRequired, got {other:?}"),
2441 }
2442
2443 let salt = Word::from([13u32, 14, 15, 16]);
2444 assert_eq!(
2445 try_injected_auth_arg(fee_conversion_request(), &smart_multisig_account(), 500)
2446 .expect("a declared salt is accepted"),
2447 Some(native_commitment(salt)),
2448 "a smart multisig account that declares a salt commits the native conversion info"
2449 );
2450
2451 assert_eq!(
2452 try_injected_auth_arg(
2453 TransactionRequestBuilder::new().build().unwrap(),
2454 &smart_multisig_account(),
2455 0
2456 )
2457 .expect("a chain charging nothing needs no conversion info"),
2458 None,
2459 );
2460 }
2461
2462 #[test]
2465 fn an_unrecognized_auth_component_is_rejected_rather_than_panicking() {
2466 const CUSTOM_AUTH: &str = "
2467 use miden::protocol::native_account
2468
2469 @auth_script
2470 pub proc auth_custom
2471 exec.native_account::incr_nonce
2472 drop
2473 end
2474 ";
2475
2476 let code = CodeBuilder::default()
2477 .compile_component_code("miden::testing::custom_auth", CUSTOM_AUTH)
2478 .expect("custom auth component code should compile");
2479 let auth = AccountComponent::new(
2480 code,
2481 vec![],
2482 AccountComponentMetadata::new("miden::testing::custom_auth"),
2483 )
2484 .expect("custom auth component");
2485
2486 let account = account_with_auth(auth);
2487
2488 let err = validate_fee_conversion_info_support(
2489 &fee_conversion_request(),
2490 &account.code_interface(),
2491 )
2492 .expect_err("an account with no recognized auth component cannot read conversion info");
2493 assert!(matches!(
2494 err,
2495 ClientError::TransactionRequestError(
2496 TransactionRequestError::FeeConversionInfoUnsupported(_)
2497 )
2498 ));
2499
2500 assert_eq!(
2501 try_injected_auth_arg(TransactionRequestBuilder::new().build().unwrap(), &account, 500)
2502 .expect("a request declaring nothing is left alone"),
2503 None,
2504 "an auth component nothing can reason about gets nothing attached"
2505 );
2506 }
2507}