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};
75use miden_protocol::crypto::SequentialCommit;
76use miden_protocol::errors::AssetError;
77use miden_protocol::note::{
78 Note,
79 NoteAttachments,
80 NoteDetails,
81 NoteId,
82 NoteRecipient,
83 NoteScript,
84 NoteTag,
85};
86use miden_protocol::protocol_config::ProtocolConfig;
87use miden_protocol::transaction::PartialBlockchain;
88use miden_protocol::vm::MIN_STACK_DEPTH;
89use miden_protocol::{Felt, Word};
90use miden_standards::account::auth::{
91 FeeConversionInfo,
92 MultisigAuthArgs,
93 commit_fee_conversion_info,
94};
95use miden_standards::account::faucets::FungibleFaucet;
96use miden_standards::account::interface::AccountComponentInterfaceExt;
97use miden_standards::note::TxFeeNote;
98use miden_tx::{DataStore, NoteConsumptionChecker, TransactionExecutor};
99use tracing::info;
100
101use super::Client;
102use crate::ClientError;
103use crate::note::{NoteScreenerError, NoteUpdateTracker, StandardNote};
104use crate::rpc::domain::account::{
105 AccountStorageRequirements,
106 GetAccountRequest,
107 StorageMapFetch,
108 VaultFetch,
109};
110use crate::rpc::encryption::{TransactionEncryptionKey, seal_transaction_inputs};
111use crate::rpc::{AccountStateAt, NodeRpcClient, RpcError};
112use crate::store::data_store::{ClientDataStore, build_partial_mmr_and_headers_with_fallback};
113use crate::store::input_note_states::ExpectedNoteState;
114use crate::store::{
115 AccountRecord,
116 InputNoteRecord,
117 InputNoteState,
118 NoteFilter,
119 NoteRecordError,
120 OutputNoteRecord,
121 Store,
122 StoreError,
123 TransactionFilter,
124};
125use crate::sync::NoteTagRecord;
126
127pub mod batch;
128pub use batch::{BatchBuilder, BatchBuilderError, ProvenBatchSubmission};
129
130mod chain_anchor;
131pub use chain_anchor::{ChainAnchor, ChainAnchorError};
132
133#[cfg(feature = "dap")]
134mod dap_executor;
135mod prover;
136pub use prover::TransactionProver;
137
138mod record;
139pub use record::{
140 DiscardCause,
141 TransactionDetails,
142 TransactionRecord,
143 TransactionStatus,
144 TransactionStatusVariant,
145};
146
147mod store_update;
148pub use store_update::TransactionStoreUpdate;
149
150mod request;
151pub use request::{
152 ForeignAccount,
153 NoteArgs,
154 PaymentNoteDescription,
155 PswapTransactionData,
156 SwapTransactionData,
157 TransactionRequest,
158 TransactionRequestBuilder,
159 TransactionRequestError,
160 TransactionScriptTemplate,
161 build_fpi_script,
162};
163
164mod observer;
165pub use observer::TransactionObserver;
166
167mod result;
168pub use miden_protocol::transaction::{
171 AccountInputs,
172 ExecutedTransaction,
173 InputNote,
174 InputNotes,
175 OutputNote,
176 OutputNotes,
177 ProvenTransaction,
178 PublicOutputNote,
179 RawOutputNote,
180 RawOutputNotes,
181 TransactionArgs,
182 TransactionFee,
183 TransactionFeeError,
184 TransactionId,
185 TransactionInputs,
186 TransactionKernel,
187 TransactionScript,
188 TransactionScriptRoot,
189 TransactionSummary,
190};
191pub use miden_protocol::vm::{AdviceInputs, AdviceMap};
192pub use miden_standards::account::interface::{AccountComponentInterface, AccountInterface};
193pub use miden_standards::tx_script::{
194 ExpirationTransactionScript,
195 SendNotesTransactionScriptError,
196};
197pub use miden_tx::auth::TransactionAuthenticator;
198pub use miden_tx::{
199 DataStoreError,
200 LocalTransactionProver,
201 Prover,
202 TransactionExecutorError,
203 TransactionProverError,
204};
205pub use result::TransactionResult;
206
207pub(crate) const NATIVE_FEE_CONVERSION_SALT: Word = Word::empty();
215
216impl<AUTH> Client<AUTH>
218where
219 AUTH: TransactionAuthenticator + Sync + 'static,
220{
221 pub async fn get_transactions(
226 &self,
227 filter: TransactionFilter,
228 ) -> Result<Vec<TransactionRecord>, ClientError> {
229 self.store.get_transactions(filter).await.map_err(Into::into)
230 }
231
232 pub async fn submit_new_transaction(
240 &mut self,
241 account_id: AccountId,
242 transaction_request: TransactionRequest,
243 ) -> Result<TransactionId, ClientError> {
244 let prover = self.tx_prover.clone();
245 self.submit_new_transaction_with_prover(account_id, transaction_request, prover)
246 .await
247 }
248
249 pub async fn submit_new_transaction_with_prover(
255 &mut self,
256 account_id: AccountId,
257 transaction_request: TransactionRequest,
258 tx_prover: Arc<dyn TransactionProver>,
259 ) -> Result<TransactionId, ClientError> {
260 if !transaction_request.expected_ntx_scripts().is_empty() {
263 Box::pin(self.ensure_ntx_scripts_registered(
264 account_id,
265 transaction_request.expected_ntx_scripts(),
266 tx_prover.clone(),
267 ))
268 .await?;
269 }
270
271 let tx_result = self.execute_transaction(account_id, transaction_request).await?;
272 let tx_id = tx_result.executed_transaction().id();
273
274 let proven_transaction = self.prove_transaction_with(&tx_result, tx_prover).await?;
275 let submission_height =
276 self.submit_proven_transaction(proven_transaction, &tx_result).await?;
277
278 let tx_update =
286 Box::new(self.get_transaction_store_update(&tx_result, submission_height).await?);
287
288 if let Err(apply_err) = self.apply_transaction_update((*tx_update).clone()).await {
289 info!(
290 "apply_transaction_update failed for submitted tx {tx_id}; returning \
291 ApplyTransactionAfterSubmitFailed with the pending update attached: {apply_err}"
292 );
293 return Err(ClientError::ApplyTransactionAfterSubmitFailed {
294 pending_update: tx_update,
295 source: Box::new(apply_err),
296 });
297 }
298
299 for observer in &self.transaction_observers {
303 crate::errors::log_observer_failure(
304 observer.name(),
305 "TransactionObserver::apply",
306 observer.apply(&tx_result).await,
307 );
308 }
309
310 Ok(tx_id)
311 }
312
313 pub async fn execute_transaction(
323 &self,
324 account_id: AccountId,
325 transaction_request: TransactionRequest,
326 ) -> Result<TransactionResult, ClientError> {
327 Box::pin(self.execute_transaction_with_mode(
328 account_id,
329 transaction_request,
330 TransactionExecutionMode::Standard,
331 None,
332 ))
333 .await
334 }
335
336 pub async fn execute_transaction_at(
370 &mut self,
371 account_id: AccountId,
372 transaction_request: TransactionRequest,
373 anchor: ChainAnchor,
374 ) -> Result<TransactionResult, ClientError> {
375 let result = self
376 .execute_transaction_with_mode(
377 account_id,
378 transaction_request,
379 TransactionExecutionMode::Standard,
380 Some(Box::new(anchor)),
381 )
382 .await?;
383
384 let expiration = result.executed_transaction().expiration_block_num();
389 let sync_height = self.store.get_sync_height().await?;
390 if expiration <= sync_height {
391 return Err(
392 ChainAnchorError::AnchoredTransactionExpired { expiration, sync_height }.into()
393 );
394 }
395
396 Ok(result)
397 }
398
399 async fn chain_anchor_at_tip(
404 &self,
405 tracked_blocks: BTreeSet<BlockNumber>,
406 ) -> Result<ChainAnchor, ClientError> {
407 let sync_height = self.store.get_sync_height().await?;
408
409 let (header, _had_notes) = self
410 .store
411 .get_block_header_by_num(sync_height)
412 .await?
413 .ok_or(StoreError::BlockHeaderNotFound(sync_height))?;
414
415 let mut tracked_blocks = tracked_blocks;
416 tracked_blocks.remove(&sync_height);
418 if let Some(&future_block) =
419 tracked_blocks.iter().find(|&&block_num| block_num > sync_height)
420 {
421 return Err(StoreError::BlockHeaderNotFound(future_block).into());
422 }
423
424 let peaks = self.store.get_current_blockchain_peaks().await?;
425 let (partial_mmr, block_headers) = build_partial_mmr_and_headers_with_fallback(
426 &self.store,
427 &self.rpc_api,
428 peaks,
429 &tracked_blocks,
430 )
431 .await?;
432
433 let chain = PartialBlockchain::new(partial_mmr, block_headers)?;
434
435 Ok(ChainAnchor::new(header, chain)?)
436 }
437
438 pub async fn chain_anchor_for_request(
457 &self,
458 transaction_request: &TransactionRequest,
459 ) -> Result<ChainAnchor, ClientError> {
460 let inferred_input_note_ids: Vec<NoteId> = transaction_request
461 .input_note_ids()
462 .filter(|note_id| !transaction_request.explicit_input_notes.contains_key(note_id))
463 .collect();
464
465 let mut tracked_blocks: BTreeSet<BlockNumber> = if inferred_input_note_ids.is_empty() {
466 BTreeSet::new()
467 } else {
468 self.store
469 .get_input_notes(NoteFilter::List(inferred_input_note_ids))
470 .await?
471 .iter()
472 .filter(|record| record.is_authenticated())
473 .filter_map(|record| record.inclusion_proof())
474 .map(|proof| proof.location().block_num())
475 .collect()
476 };
477 tracked_blocks.extend(
478 transaction_request
479 .explicit_input_notes
480 .values()
481 .filter_map(InputNote::proof)
482 .map(|proof| proof.location().block_num()),
483 );
484 tracked_blocks.extend(transaction_request.block_numbers().iter().copied());
485
486 self.chain_anchor_at_tip(tracked_blocks).await
487 }
488
489 #[cfg(feature = "dap")]
503 pub async fn execute_transaction_with_dap(
504 &self,
505 account_id: AccountId,
506 transaction_request: TransactionRequest,
507 ) -> Result<TransactionResult, ClientError> {
508 self.execute_transaction_with_mode(
509 account_id,
510 transaction_request,
511 TransactionExecutionMode::Dap,
512 None,
513 )
514 .await
515 }
516
517 async fn execute_transaction_with_mode(
521 &self,
522 account_id: AccountId,
523 transaction_request: TransactionRequest,
524 execution_mode: TransactionExecutionMode,
525 anchor: Option<Box<ChainAnchor>>,
526 ) -> Result<TransactionResult, ClientError> {
527 let account: PartialAccount =
528 self.get_native_account_record(account_id).await?.try_into()?;
529
530 let prep = self
531 .prepare_transaction(&account, transaction_request, anchor.as_deref())
532 .await?;
533
534 let mut data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
535 if let Some(anchor) = anchor {
536 data_store = data_store.with_chain_anchor(*anchor);
537 }
538 data_store.register_note_scripts(prep.output_note_scripts());
539 data_store.register_block_numbers(prep.block_numbers.iter().copied());
540 for fpi_account in &prep.foreign_account_inputs {
541 data_store.mast_store().load_account_code(fpi_account.code());
542 }
543 data_store.register_foreign_account_inputs(prep.foreign_account_inputs);
544
545 data_store.mast_store().load_account_code(account.code());
546
547 let mut notes = prep.notes;
548 if prep.ignore_invalid_notes {
549 notes = self
550 .get_valid_input_notes(
551 &data_store,
552 account.id(),
553 prep.block_num,
554 notes,
555 prep.tx_args.clone(),
556 )
557 .await?;
558 }
559
560 let executed_transaction = match execution_mode {
561 TransactionExecutionMode::Standard => {
562 self.build_executor(&data_store)?
563 .execute_transaction(account_id, prep.block_num, notes, prep.tx_args)
564 .await?
565 },
566 #[cfg(feature = "dap")]
567 TransactionExecutionMode::Dap => {
568 self.build_dap_executor(&data_store)?
569 .execute_transaction(account_id, prep.block_num, notes, prep.tx_args)
570 .await?
571 },
572 };
573
574 validate_executed_transaction(&executed_transaction, &prep.output_recipients)?;
575 TransactionResult::new(executed_transaction, prep.future_notes)
576 }
577
578 pub(crate) async fn prepare_transaction(
594 &self,
595 account: &PartialAccount,
596 transaction_request: TransactionRequest,
597 anchor: Option<&ChainAnchor>,
598 ) -> Result<PreparedTransaction, ClientError> {
599 self.validate_account_request(
600 &transaction_request,
601 account.id(),
602 &account.code_interface(),
603 )
604 .await?;
605
606 self.prepare_transaction_inner(account.code_interface(), transaction_request, anchor)
607 .await
608 }
609
610 pub(crate) async fn prepare_transaction_for_batch(
611 &self,
612 account: &PartialAccount,
613 transaction_request: TransactionRequest,
614 ) -> Result<PreparedTransaction, ClientError> {
615 self.prepare_transaction_inner(account.code_interface(), transaction_request, None)
616 .await
617 }
618
619 async fn prepare_transaction_inner(
620 &self,
621 account_code_interface: AccountCodeInterface,
622 mut transaction_request: TransactionRequest,
623 anchor: Option<&ChainAnchor>,
624 ) -> Result<PreparedTransaction, ClientError> {
625 if anchor.is_none() {
626 self.validate_recency().await?;
627 }
628
629 let mut stored_note_records = self
631 .store
632 .get_input_notes(NoteFilter::List(transaction_request.input_note_ids().collect()))
633 .await?;
634
635 for note in &stored_note_records {
640 if note.is_consumed() {
641 return Err(ClientError::TransactionRequestError(
642 TransactionRequestError::InputNoteAlreadyConsumed(note.details_commitment()),
643 ));
644 }
645 if let Some(transaction_id) = note.consumer_transaction_id()
646 && note.is_processing()
647 {
648 return Err(ClientError::TransactionRequestError(
649 TransactionRequestError::InputNoteBeingProcessed {
650 note: note.details_commitment(),
651 transaction_id: *transaction_id,
652 },
653 ));
654 }
655 }
656
657 stored_note_records.retain(InputNoteRecord::is_authenticated);
659
660 let notes = transaction_request.build_input_notes(stored_note_records)?;
661
662 if let Some(anchor) = anchor {
666 for note in notes.iter() {
667 if let Some(location) = note.location() {
668 let block_num = location.block_num();
669 if block_num < anchor.block_num()
670 && !anchor.partial_blockchain().contains_block(block_num)
671 {
672 return Err(ChainAnchorError::BlockNotTracked { block_num }.into());
673 }
674 }
675 }
676 }
677
678 let output_recipients =
679 transaction_request.expected_output_recipients().cloned().collect::<Vec<_>>();
680
681 let future_notes: Vec<(NoteDetails, NoteTag)> =
682 transaction_request.expected_future_notes().cloned().collect();
683
684 let tx_script = transaction_request.build_transaction_script(&account_code_interface)?;
685
686 let foreign_accounts = transaction_request.foreign_accounts().clone();
687
688 let block_num = match anchor {
691 Some(anchor) => anchor.block_num(),
692 None => self.store.get_sync_height().await?,
693 };
694
695 let foreign_account_inputs = self
696 .get_foreign_account_inputs(foreign_accounts.into_values(), block_num)
697 .await?;
698
699 let ignore_invalid_notes = transaction_request.ignore_invalid_input_notes();
700 let mut block_numbers = transaction_request.block_numbers().clone();
701 block_numbers.extend(
702 AuthArgsLayout::of(&account_code_interface)
703 .bound_block(&transaction_request)
704 .filter(|bound_block| *bound_block < block_num),
705 );
706
707 let reference_header = match anchor {
708 Some(anchor) => anchor.header().clone(),
709 None => {
710 self.store
711 .get_block_header_by_num(block_num)
712 .await?
713 .ok_or(StoreError::BlockHeaderNotFound(block_num))?
714 .0
715 },
716 };
717
718 for inputs in &foreign_account_inputs {
721 if inputs.compute_account_root().ok() != Some(reference_header.account_root()) {
722 return Err(TransactionRequestError::ForeignAccountNotAtReferenceBlock {
723 account_id: inputs.id(),
724 block_num,
725 }
726 .into());
727 }
728 }
729
730 attach_default_auth_args(
731 &mut transaction_request,
732 &account_code_interface,
733 &reference_header,
734 &self.get_protocol_config(reference_header.protocol_config_commitment()).await?,
735 )?;
736
737 let tx_args = transaction_request.into_transaction_args(tx_script);
738
739 Ok(PreparedTransaction {
740 notes,
741 output_recipients,
742 future_notes,
743 tx_args,
744 foreign_account_inputs,
745 block_numbers,
746 block_num,
747 ignore_invalid_notes,
748 })
749 }
750
751 pub async fn prove_transaction(
753 &self,
754 tx_result: &TransactionResult,
755 ) -> Result<ProvenTransaction, ClientError> {
756 self.prove_transaction_with(tx_result, self.tx_prover.clone()).await
757 }
758
759 pub async fn prove_transaction_with(
767 &self,
768 tx_result: &TransactionResult,
769 tx_prover: Arc<dyn TransactionProver>,
770 ) -> Result<ProvenTransaction, ClientError> {
771 info!("Proving transaction...");
772
773 let executed_transaction = tx_result.executed_transaction();
774 let proven_transaction = tx_prover.prove(executed_transaction.clone().into()).await?;
775
776 if proven_transaction.id() != executed_transaction.id() {
785 return Err(ClientError::MismatchedProvenTransaction {
786 requested: executed_transaction.id(),
787 returned: proven_transaction.id(),
788 });
789 }
790
791 info!("Transaction proven.");
792
793 Ok(proven_transaction)
794 }
795
796 pub async fn submit_proven_transaction(
806 &mut self,
807 proven_transaction: ProvenTransaction,
808 transaction_inputs: impl Into<TransactionInputs>,
809 ) -> Result<BlockNumber, ClientError> {
810 let account_id = proven_transaction.account_id();
812 if self.is_allowlist_gated(account_id).await? {
813 ensure_account_allowed(account_id, self.is_account_allowed(account_id).await)?;
814 }
815
816 info!("Submitting transaction to the network...");
817 let tx_id = proven_transaction.id();
818 let key = self.transaction_encryption_key().await?;
819
820 let transaction_inputs = transaction_inputs.into();
824
825 let sealed_inputs =
826 seal_transaction_inputs(&mut self.rng, &key, tx_id, &transaction_inputs)?;
827
828 let result =
829 self.rpc_api.submit_proven_transaction(&proven_transaction, sealed_inputs).await;
830 if let Err(err) = &result {
831 self.forget_stale_transaction_encryption_key(err).await;
832 }
833
834 let block_num = result.map_err(|err| {
835 promote_indeterminate_submission(err, proven_transaction, transaction_inputs)
836 })?;
837 info!("Transaction submitted.");
838
839 Ok(block_num)
840 }
841
842 pub(crate) async fn transaction_encryption_key(
850 &self,
851 ) -> Result<TransactionEncryptionKey, ClientError> {
852 if let Some(key) = self.store.get_transaction_encryption_key().await? {
853 return Ok(key);
854 }
855
856 let attested = self.rpc_api.get_transaction_encryption_key().await?;
857
858 let genesis_commitment =
862 self.trusted_block_header(BlockNumber::GENESIS).await?.commitment();
863 let validator_keys = self.get_validator_config().await?;
864
865 let key = attested.verify(genesis_commitment, &validator_keys)?;
866 self.store.set_transaction_encryption_key(&key).await?;
867
868 Ok(key)
869 }
870
871 #[cfg(feature = "testing")]
874 pub async fn seed_transaction_encryption_key(
875 &self,
876 key: TransactionEncryptionKey,
877 ) -> Result<(), ClientError> {
878 Ok(self.store.set_transaction_encryption_key(&key).await?)
879 }
880
881 pub(crate) async fn forget_stale_transaction_encryption_key(&self, err: &RpcError) {
887 if err.is_stale_transaction_encryption_key()
888 && let Err(err) = self.store.remove_transaction_encryption_key().await
889 {
890 tracing::warn!("failed to evict the stale transaction encryption key: {err}");
891 }
892 }
893
894 async fn trusted_block_header(
901 &self,
902 block_num: BlockNumber,
903 ) -> Result<BlockHeader, ClientError> {
904 self.store.get_block_header_by_num(block_num).await?.map(|(header, _)| header).ok_or_else(
905 || {
906 ClientError::ChainValidationError(alloc::format!(
907 "block header {block_num} is not tracked locally; sync the client before it can verify data against the chain"
908 ))
909 },
910 )
911 }
912
913 pub async fn get_transaction_store_update(
916 &self,
917 tx_result: &TransactionResult,
918 submission_height: BlockNumber,
919 ) -> Result<TransactionStoreUpdate, TransactionStoreUpdateError> {
920 let note_updates = self.get_note_updates(submission_height, tx_result).await?;
921
922 let new_tags: Vec<NoteTagRecord> = note_updates
925 .updated_input_notes()
926 .filter_map(|note| {
927 let note = note.inner();
928
929 if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) =
930 note.state()
931 {
932 Some(NoteTagRecord::with_note_source(*tag, note.details_commitment()))
933 } else {
934 None
935 }
936 })
937 .collect();
938
939 Ok(TransactionStoreUpdate::new(
940 tx_result.executed_transaction().clone(),
941 submission_height,
942 note_updates,
943 tx_result.future_notes().to_vec(),
944 new_tags,
945 ))
946 }
947
948 pub async fn apply_transaction(
951 &self,
952 tx_result: &TransactionResult,
953 submission_height: BlockNumber,
954 ) -> Result<(), ClientError> {
955 let tx_update = self.get_transaction_store_update(tx_result, submission_height).await?;
956
957 self.apply_transaction_update(tx_update).await?;
958
959 for observer in &self.transaction_observers {
961 if let Err(err) = observer.apply(tx_result).await {
962 tracing::warn!(
963 observer = observer.name(),
964 error = ?err,
965 "TransactionObserver::apply failed; continuing with remaining observers",
966 );
967 }
968 }
969
970 Ok(())
971 }
972
973 pub async fn apply_transaction_update(
974 &self,
975 tx_update: TransactionStoreUpdate,
976 ) -> Result<(), ClientError> {
977 info!("Applying transaction to the local store...");
980
981 let executed_transaction = tx_update.executed_transaction();
982 let account_id = executed_transaction.account_id();
983
984 if self.account_reader(account_id).status().await?.is_locked() {
985 return Err(ClientError::AccountLocked(account_id));
986 }
987
988 self.store.apply_transaction(tx_update).await?;
989 info!("Transaction stored.");
990 Ok(())
991 }
992
993 pub async fn execute_program(
998 &self,
999 account_id: AccountId,
1000 tx_script: TransactionScript,
1001 advice_inputs: AdviceInputs,
1002 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
1003 ) -> Result<[Felt; MIN_STACK_DEPTH], ClientError> {
1004 let (data_store, block_ref) =
1005 self.prepare_program_execution(account_id, foreign_accounts).await?;
1006
1007 Ok(self
1008 .build_executor(&data_store)?
1009 .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
1010 .await?)
1011 }
1012
1013 #[cfg(feature = "dap")]
1016 pub async fn execute_program_with_dap(
1017 &self,
1018 account_id: AccountId,
1019 tx_script: TransactionScript,
1020 advice_inputs: AdviceInputs,
1021 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
1022 ) -> Result<[Felt; MIN_STACK_DEPTH], ClientError> {
1023 let (data_store, block_ref) =
1024 self.prepare_program_execution(account_id, foreign_accounts).await?;
1025
1026 Ok(self
1027 .build_dap_executor(&data_store)?
1028 .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
1029 .await?)
1030 }
1031
1032 pub async fn validate_request(
1042 &self,
1043 account_id: AccountId,
1044 transaction_request: &TransactionRequest,
1045 ) -> Result<(), ClientError> {
1046 self.validate_recency().await?;
1047 validate_output_note_senders(transaction_request, account_id)?;
1048 let account: PartialAccount = self
1049 .store
1050 .get_minimal_partial_account(account_id)
1051 .await?
1052 .ok_or(ClientError::AccountDataNotFound(account_id))?
1053 .try_into()?;
1054 self.validate_account_request(transaction_request, account_id, &account.code_interface())
1055 .await
1056 }
1057
1058 async fn validate_account_request(
1063 &self,
1064 transaction_request: &TransactionRequest,
1065 account_id: AccountId,
1066 account_code_interface: &AccountCodeInterface,
1067 ) -> Result<(), ClientError> {
1068 if account_code_interface.contains([FungibleFaucet::mint_and_send_root()]) {
1069 Ok(())
1071 } else {
1072 let assets = self.account_reader(account_id).assets().await?;
1073 validate_basic_account_request(transaction_request, &assets)
1074 }
1075 }
1076
1077 async fn validate_recency(&self) -> Result<(), ClientError> {
1078 if let Some(max_block_number_delta) = self.max_block_number_delta {
1079 let current_chain_tip =
1080 self.rpc_api.get_block_header_by_number(None, false).await?.0.block_num();
1081
1082 if current_chain_tip > self.store.get_sync_height().await? + max_block_number_delta {
1083 return Err(ClientError::RecencyConditionError(
1084 "The client is too far behind the chain tip to execute the transaction",
1085 ));
1086 }
1087 }
1088 Ok(())
1089 }
1090
1091 pub async fn ensure_ntx_scripts_registered(
1104 &mut self,
1105 account_id: AccountId,
1106 scripts: &[NoteScript],
1107 tx_prover: Arc<dyn TransactionProver>,
1108 ) -> Result<(), ClientError> {
1109 let mut missing_scripts = Vec::new();
1110
1111 for script in scripts {
1112 if StandardNote::from_script(script).is_some() {
1114 continue;
1115 }
1116
1117 let script_root = script.root();
1118
1119 match self.rpc_api.get_note_script_by_root(script_root.into()).await {
1121 Ok(Some(_)) => {},
1122 Ok(None) => missing_scripts.push(script.clone()),
1123 Err(source) => {
1124 return Err(ClientError::NtxScriptRegistrationFailed {
1125 script_root: script_root.into(),
1126 source,
1127 });
1128 },
1129 }
1130 }
1131
1132 if missing_scripts.is_empty() {
1133 return Ok(());
1134 }
1135
1136 let registration_request = TransactionRequestBuilder::new().build_register_note_scripts(
1137 account_id,
1138 missing_scripts,
1139 self.rng(),
1140 )?;
1141
1142 let tx_result = self.execute_transaction(account_id, registration_request).await?;
1143 let proven = self.prove_transaction_with(&tx_result, tx_prover).await?;
1144 let submission_height = self.submit_proven_transaction(proven, &tx_result).await?;
1145 self.apply_transaction(&tx_result, submission_height).await?;
1146
1147 Ok(())
1148 }
1149
1150 pub(crate) async fn get_valid_input_notes<STORE: DataStore + Sync>(
1159 &self,
1160 data_store: &STORE,
1161 account_id: AccountId,
1162 block_ref: BlockNumber,
1163 mut input_notes: InputNotes<InputNote>,
1164 tx_args: TransactionArgs,
1165 ) -> Result<InputNotes<InputNote>, ClientError> {
1166 loop {
1167 if input_notes.is_empty() {
1170 break;
1171 }
1172
1173 let execution = NoteConsumptionChecker::new(&self.build_executor(data_store)?)
1174 .check_notes_consumability(
1175 account_id,
1176 block_ref,
1177 input_notes.iter().map(|n| n.clone().into_note()).collect(),
1178 tx_args.clone(),
1179 )
1180 .await?;
1181
1182 if execution.failed().is_empty() {
1183 break;
1184 }
1185
1186 let failed_note_ids: BTreeSet<NoteId> =
1187 execution.failed().iter().map(|n| n.note().id()).collect();
1188 let filtered_input_notes = InputNotes::new(
1189 input_notes
1190 .into_iter()
1191 .filter(|note| !failed_note_ids.contains(¬e.id()))
1192 .collect(),
1193 )
1194 .expect("Created from a valid input notes list");
1195
1196 input_notes = filtered_input_notes;
1197 }
1198
1199 Ok(input_notes)
1200 }
1201
1202 pub async fn get_foreign_account_inputs(
1224 &self,
1225 foreign_accounts: impl IntoIterator<Item = ForeignAccount>,
1226 block_num: BlockNumber,
1227 ) -> Result<Vec<AccountInputs>, ClientError> {
1228 let foreign_accounts = foreign_accounts.into_iter();
1229 let mut return_foreign_account_inputs = Vec::with_capacity(foreign_accounts.size_hint().0);
1230
1231 for foreign_account in foreign_accounts {
1232 let foreign_account_inputs = match foreign_account {
1233 ForeignAccount::Public(account_id, storage_requirements) => {
1234 fetch_public_account_inputs(
1235 &self.store,
1236 &self.rpc_api,
1237 account_id,
1238 storage_requirements,
1239 AccountStateAt::Block(block_num),
1240 )
1241 .await?
1242 },
1243 ForeignAccount::Private(partial_account) => {
1244 let witness =
1247 self.get_account_witness_at(partial_account.id(), block_num).await?;
1248
1249 AccountInputs::new(partial_account, witness)
1250 },
1251 ForeignAccount::Prefetched(inputs) => inputs,
1252 };
1253
1254 return_foreign_account_inputs.push(foreign_account_inputs);
1255 }
1256
1257 Ok(return_foreign_account_inputs)
1258 }
1259
1260 async fn get_account_witness_at(
1267 &self,
1268 account_id: AccountId,
1269 block_num: BlockNumber,
1270 ) -> Result<AccountWitness, ClientError> {
1271 if block_num == self.store.get_sync_height().await?
1273 && let Some(witness) = self.store.get_account_witness(account_id).await?
1274 {
1275 return Ok(witness);
1276 }
1277
1278 let (_, account_proof) = self
1279 .rpc_api
1280 .get_account(account_id, GetAccountRequest::new().at(AccountStateAt::Block(block_num)))
1281 .await?;
1282
1283 Ok(account_proof.into_parts().0)
1284 }
1285
1286 async fn prepare_program_execution(
1290 &self,
1291 account_id: AccountId,
1292 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
1293 ) -> Result<(ClientDataStore, BlockNumber), ClientError> {
1294 let block_ref = self.get_sync_height().await?;
1295
1296 let foreign_account_inputs = self
1297 .get_foreign_account_inputs(foreign_accounts.into_values(), block_ref)
1298 .await?;
1299
1300 let account_code = self
1301 .store
1302 .get_account_code(account_id)
1303 .await?
1304 .ok_or(ClientError::AccountDataNotFound(account_id))?;
1305
1306 let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
1307
1308 data_store.mast_store().load_account_code(&account_code);
1310
1311 for fpi_account in &foreign_account_inputs {
1312 data_store.mast_store().load_account_code(fpi_account.code());
1313 }
1314
1315 data_store.register_foreign_account_inputs(foreign_account_inputs);
1316
1317 Ok((data_store, block_ref))
1318 }
1319
1320 pub(crate) fn build_executor<'store, 'auth, STORE: DataStore + Sync>(
1323 &'auth self,
1324 data_store: &'store STORE,
1325 ) -> Result<TransactionExecutor<'store, 'auth, STORE, AUTH>, TransactionExecutorError> {
1326 let mut executor = TransactionExecutor::new(data_store)
1327 .with_options(self.exec_options)?
1328 .with_source_manager(self.source_manager.clone());
1329 if let Some(authenticator) = self.authenticator.as_deref() {
1330 executor = executor.with_authenticator(authenticator);
1331 }
1332 Ok(executor)
1333 }
1334
1335 async fn get_native_account_record(
1340 &self,
1341 account_id: AccountId,
1342 ) -> Result<AccountRecord, ClientError> {
1343 let account_record = self
1344 .store
1345 .get_minimal_partial_account(account_id)
1346 .await?
1347 .ok_or(ClientError::AccountDataNotFound(account_id))?;
1348 if account_record.is_watched() {
1349 return Err(ClientError::AccountIsWatched(account_id));
1350 }
1351 Ok(account_record)
1352 }
1353
1354 #[cfg(feature = "dap")]
1356 pub(crate) fn build_dap_executor<'store, 'auth, STORE: DataStore + Sync>(
1357 &'auth self,
1358 data_store: &'store STORE,
1359 ) -> Result<
1360 TransactionExecutor<'store, 'auth, STORE, AUTH, dap_executor::DapProgramExecutor>,
1361 TransactionExecutorError,
1362 > {
1363 Ok(self
1364 .build_executor(data_store)?
1365 .with_program_executor::<dap_executor::DapProgramExecutor>())
1366 }
1367
1368 async fn get_note_updates(
1371 &self,
1372 submission_height: BlockNumber,
1373 tx_result: &TransactionResult,
1374 ) -> Result<NoteUpdateTracker, TransactionStoreUpdateError> {
1375 let executed_tx = tx_result.executed_transaction();
1376 let current_timestamp = self.store.get_current_timestamp();
1377 let current_block_num = self.store.get_sync_height().await?;
1378
1379 let new_output_notes = executed_tx
1391 .output_notes()
1392 .iter()
1393 .filter(|output_note| {
1394 output_note
1395 .recipient()
1396 .is_none_or(|recipient| recipient.script().root() != TxFeeNote::script_root())
1397 })
1398 .cloned()
1399 .filter_map(|output_note| {
1400 OutputNoteRecord::try_from_output_note(output_note, submission_height).ok()
1401 })
1402 .collect::<Vec<_>>();
1403
1404 let mut new_input_notes = vec![];
1406 let output_notes: Vec<Note> =
1407 notes_from_output(executed_tx.output_notes()).cloned().collect();
1408 let note_screener = self.note_screener().clone();
1409 let output_note_relevances = note_screener.get_batch_consumability(&output_notes).await?;
1410
1411 for note in output_notes {
1412 if note.script().root() == TxFeeNote::script_root() {
1417 continue;
1418 }
1419
1420 if output_note_relevances.contains_key(¬e.id()) {
1421 let metadata = *note.metadata();
1422 let tag = metadata.tag();
1423 let attachments = note.attachments().clone();
1424
1425 new_input_notes.push(InputNoteRecord::new(
1426 note.into(),
1427 attachments,
1428 current_timestamp,
1429 ExpectedNoteState {
1430 metadata: Some(metadata),
1431 after_block_num: submission_height,
1432 tag: Some(tag),
1433 }
1434 .into(),
1435 ));
1436 }
1437 }
1438
1439 new_input_notes.extend(tx_result.future_notes().iter().map(|(note_details, tag)| {
1441 InputNoteRecord::new(
1442 note_details.clone(),
1443 NoteAttachments::empty(),
1444 None,
1445 ExpectedNoteState {
1446 metadata: None,
1447 after_block_num: current_block_num,
1448 tag: Some(*tag),
1449 }
1450 .into(),
1451 )
1452 }));
1453
1454 let consumed_note_ids =
1459 executed_tx.tx_inputs().input_notes().iter().map(InputNote::id).collect();
1460
1461 let consumed_notes =
1462 self.store.get_input_notes(NoteFilter::List(consumed_note_ids)).await?;
1463
1464 let tracked_note_ids =
1465 consumed_notes.iter().filter_map(InputNoteRecord::id).collect::<BTreeSet<_>>();
1466
1467 for input_note in executed_tx.tx_inputs().input_notes() {
1468 if !tracked_note_ids.contains(&input_note.id()) {
1469 let mut input_note_record = InputNoteRecord::from(input_note.clone());
1470 input_note_record.consumed_locally(
1471 executed_tx.account_id(),
1472 executed_tx.id(),
1473 current_timestamp,
1474 )?;
1475 new_input_notes.push(input_note_record);
1476 }
1477 }
1478
1479 let mut updated_input_notes = vec![];
1480
1481 for mut input_note_record in consumed_notes {
1482 if input_note_record.consumed_locally(
1483 executed_tx.account_id(),
1484 executed_tx.id(),
1485 current_timestamp,
1486 )? {
1487 updated_input_notes.push(input_note_record);
1488 }
1489 }
1490
1491 Ok(NoteUpdateTracker::for_transaction_updates(
1492 new_input_notes,
1493 updated_input_notes,
1494 new_output_notes,
1495 ))
1496 }
1497}
1498
1499#[derive(Debug, thiserror::Error)]
1505pub enum TransactionStoreUpdateError {
1506 #[error("store error")]
1507 Store(#[from] StoreError),
1508 #[error("note screener error")]
1509 NoteScreener(#[from] NoteScreenerError),
1510 #[error("note record error")]
1511 NoteRecord(#[from] NoteRecordError),
1512}
1513
1514#[derive(Clone, Copy, Debug)]
1518enum TransactionExecutionMode {
1519 Standard,
1520 #[cfg(feature = "dap")]
1521 Dap,
1522}
1523
1524pub(crate) struct PreparedTransaction {
1526 pub(crate) notes: InputNotes<InputNote>,
1527 pub(crate) output_recipients: Vec<NoteRecipient>,
1528 pub(crate) future_notes: Vec<(NoteDetails, NoteTag)>,
1529 pub(crate) tx_args: TransactionArgs,
1530 pub(crate) foreign_account_inputs: Vec<AccountInputs>,
1531 pub(crate) block_numbers: BTreeSet<BlockNumber>,
1532 pub(crate) block_num: BlockNumber,
1533 pub(crate) ignore_invalid_notes: bool,
1534}
1535
1536impl PreparedTransaction {
1537 pub(crate) fn output_note_scripts(&self) -> impl Iterator<Item = NoteScript> + '_ {
1540 self.output_recipients.iter().map(|recipient| recipient.script().clone())
1541 }
1542}
1543
1544fn get_outgoing_assets(
1549 transaction_request: &TransactionRequest,
1550) -> (BTreeMap<AccountId, u64>, Vec<Asset>) {
1551 let mut own_notes_assets = match transaction_request.script_template() {
1552 Some(TransactionScriptTemplate::SendNotes(notes)) => notes
1553 .iter()
1554 .map(|note| (note.id(), note.assets().clone()))
1555 .collect::<BTreeMap<_, _>>(),
1556 _ => BTreeMap::default(),
1557 };
1558 let mut output_notes_assets = transaction_request
1559 .expected_output_own_notes()
1560 .into_iter()
1561 .map(|note| (note.id(), note.assets().clone()))
1562 .collect::<BTreeMap<_, _>>();
1563
1564 output_notes_assets.append(&mut own_notes_assets);
1566
1567 let outgoing_assets = output_notes_assets.values().flat_map(|note_assets| note_assets.iter());
1569
1570 request::collect_assets(outgoing_assets)
1571}
1572
1573fn attach_default_auth_args(
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 if let Some((auth_arg, preimage)) = AuthArgsLayout::of(account_code_interface)
1592 .default_auth_args(
1593 transaction_request.fee_conversion_salt(),
1594 reference_header,
1595 protocol_config,
1596 )?
1597 {
1598 transaction_request.commit_auth_args(auth_arg, preimage);
1599 }
1600
1601 Ok(())
1602}
1603
1604pub(crate) enum AuthArgsLayout {
1606 SingleSig,
1609 Multisig(String),
1612 Unsupported(String),
1615}
1616
1617impl AuthArgsLayout {
1618 pub(crate) fn of(account_code_interface: &AccountCodeInterface) -> Self {
1623 let procedures: Vec<_> = account_code_interface.procedures().iter().copied().collect();
1624
1625 AccountComponentInterface::from_procedures(&procedures)
1626 .iter()
1627 .find_map(|component| match component {
1628 AccountComponentInterface::AuthSingleSig => Some(Self::SingleSig),
1629 AccountComponentInterface::AuthMultisig
1630 | AccountComponentInterface::AuthMultisigSmart
1631 | AccountComponentInterface::AuthGuardedMultisig => {
1632 Some(Self::Multisig(component.name()))
1633 },
1634 AccountComponentInterface::AuthNoAuth
1635 | AccountComponentInterface::AuthNetworkAccount => {
1636 Some(Self::Unsupported(component.name()))
1637 },
1638 _ => None,
1639 })
1640 .unwrap_or_else(|| Self::Unsupported("unrecognized".into()))
1641 }
1642
1643 fn bound_block(&self, transaction_request: &TransactionRequest) -> Option<BlockNumber> {
1648 if !matches!(self, Self::Multisig(_)) || !transaction_request.has_auth_arg() {
1649 return None;
1650 }
1651
1652 let auth_arg = (*transaction_request.auth_arg())?;
1653 let preimage = transaction_request.advice_map().get(&auth_arg)?;
1654 let bound_block = u32::try_from(preimage.first()?.as_canonical_u64()).ok()?;
1655
1656 Some(BlockNumber::from(bound_block))
1657 }
1658
1659 pub(crate) fn default_auth_args(
1670 &self,
1671 declared_salt: Option<Word>,
1672 reference_header: &BlockHeader,
1673 protocol_config: &ProtocolConfig,
1674 ) -> Result<Option<(Word, Vec<Felt>)>, TransactionRequestError> {
1675 let fee_charged = reference_header.fee_parameters().verification_base_fee() != 0;
1676 let conversion_info =
1677 FeeConversionInfo::one_to_one(protocol_config.fee_asset_id().faucet_id());
1678
1679 match (self, declared_salt) {
1680 (Self::SingleSig, None) if !fee_charged => Ok(None),
1681 (Self::SingleSig, salt) => Ok(Some(commit_fee_conversion_info(
1682 conversion_info,
1683 salt.unwrap_or(NATIVE_FEE_CONVERSION_SALT),
1684 ))),
1685 (Self::Multisig(_), Some(salt)) => {
1686 let auth_args = MultisigAuthArgs::new(reference_header.block_num(), salt)
1687 .with_conversion_info(conversion_info);
1688 Ok(Some((auth_args.to_commitment(), auth_args.to_elements())))
1689 },
1690 (Self::Multisig(component), None) => {
1691 Err(TransactionRequestError::FeeConversionInfoRequired(component.clone()))
1692 },
1693 (Self::Unsupported(component), Some(_)) => {
1694 Err(TransactionRequestError::FeeConversionInfoUnsupported(component.clone()))
1695 },
1696 (Self::Unsupported(_), None) => Ok(None),
1697 }
1698 }
1699}
1700
1701fn validate_output_note_senders(
1709 transaction_request: &TransactionRequest,
1710 account_id: AccountId,
1711) -> Result<(), ClientError> {
1712 for note in transaction_request.expected_output_own_notes() {
1713 let sender = note.metadata().sender();
1714 if sender != account_id {
1715 return Err(ClientError::TransactionRequestError(
1716 TransactionRequestError::OutputNoteSenderMismatch {
1717 expected: account_id,
1718 actual: sender,
1719 },
1720 ));
1721 }
1722 }
1723
1724 Ok(())
1725}
1726
1727fn validate_basic_account_request(
1730 transaction_request: &TransactionRequest,
1731 vault_assets: &[Asset],
1732) -> Result<(), ClientError> {
1733 let (fungible_balance_map, non_fungible_set) = get_outgoing_assets(transaction_request);
1734 let (incoming_fungible_balance_map, incoming_non_fungible_balance_set) =
1735 transaction_request.incoming_assets();
1736
1737 let mut available_fungible: BTreeMap<AccountId, u64> = BTreeMap::new();
1740 for asset in vault_assets {
1741 if let Some(fungible) = asset.as_fungible() {
1742 let balance = available_fungible.entry(fungible.faucet_id()).or_default();
1743 *balance = balance.saturating_add(fungible.amount().as_u64());
1744 }
1745 }
1746
1747 for (faucet_id, amount) in fungible_balance_map {
1750 let account_asset_amount = available_fungible.get(&faucet_id).copied().unwrap_or(0);
1751 let incoming_balance = incoming_fungible_balance_map.get(&faucet_id).unwrap_or(&0);
1752 if account_asset_amount + incoming_balance < amount {
1753 return Err(ClientError::AssetError(AssetError::FungibleAssetAmountNotSufficient {
1754 minuend: account_asset_amount,
1755 subtrahend: amount,
1756 }));
1757 }
1758 }
1759
1760 for non_fungible in &non_fungible_set {
1763 let held = vault_assets.iter().any(|asset| asset == non_fungible);
1764 if !held && !incoming_non_fungible_balance_set.contains(non_fungible) {
1765 return Err(ClientError::TransactionRequestError(
1766 TransactionRequestError::MissingNonFungibleAsset(non_fungible.faucet_id()),
1767 ));
1768 }
1769 }
1770
1771 Ok(())
1772}
1773
1774async fn local_account_inputs(
1784 store: &Arc<dyn Store>,
1785 account_id: AccountId,
1786 account_state_at: AccountStateAt,
1787) -> Result<Option<AccountInputs>, ClientError> {
1788 if let AccountStateAt::Block(block_num) = account_state_at
1790 && block_num == store.get_sync_height().await?
1791 && let Some(witness) = store.get_account_witness(account_id).await?
1792 && let Some(record) = store.get_minimal_partial_account(account_id).await?
1793 {
1794 let account: PartialAccount = record.try_into()?;
1797 if account.to_commitment() == witness.state_commitment() {
1798 return Ok(Some(AccountInputs::new(account, witness)));
1799 }
1800 }
1801
1802 Ok(None)
1803}
1804
1805pub(crate) async fn fetch_public_account_inputs(
1816 store: &Arc<dyn Store>,
1817 rpc_api: &Arc<dyn NodeRpcClient>,
1818 account_id: AccountId,
1819 storage_requirements: AccountStorageRequirements,
1820 account_state_at: AccountStateAt,
1821) -> Result<AccountInputs, ClientError> {
1822 if let Some(inputs) = local_account_inputs(store, account_id, account_state_at).await? {
1823 return Ok(inputs);
1824 }
1825
1826 let known_code: Option<AccountCode> =
1827 store.get_foreign_account_code(vec![account_id]).await?.into_values().next();
1828
1829 let vault = store
1832 .get_account_header(account_id)
1833 .await?
1834 .map_or(VaultFetch::Always, |(header, ..)| {
1835 VaultFetch::IfChangedFrom(header.vault_root())
1836 });
1837
1838 let (_block_num, account_proof) = rpc_api
1839 .get_account(
1840 account_id,
1841 GetAccountRequest::new()
1842 .with_storage(StorageMapFetch::Slots(storage_requirements.clone()))
1843 .at(account_state_at)
1844 .with_known_code(known_code)
1845 .with_vault(vault),
1846 )
1847 .await?;
1848
1849 let account_inputs = request::account_proof_into_inputs(account_proof)?;
1850
1851 let _ = store
1852 .upsert_foreign_account_code(account_id, account_inputs.code().clone())
1853 .await
1854 .inspect_err(|err| {
1855 tracing::warn!(
1856 %account_id,
1857 %err,
1858 "Failed to persist foreign account code to store"
1859 );
1860 });
1861
1862 Ok(account_inputs)
1863}
1864
1865fn promote_indeterminate_submission(
1868 err: RpcError,
1869 transaction: ProvenTransaction,
1870 transaction_inputs: TransactionInputs,
1871) -> ClientError {
1872 if !err.is_indeterminate_submission() {
1873 return ClientError::RpcError(err);
1874 }
1875
1876 ClientError::SubmissionOutcomeUnknown {
1877 transaction: Box::new(transaction),
1878 transaction_inputs: Box::new(transaction_inputs),
1879 source: err,
1880 }
1881}
1882
1883pub fn notes_from_output(output_notes: &RawOutputNotes) -> impl Iterator<Item = &Note> {
1888 output_notes.iter().filter_map(|n| match n {
1889 RawOutputNote::Full(n) => Some(n),
1890 RawOutputNote::Partial(_) => None,
1891 })
1892}
1893
1894pub(crate) fn validate_executed_transaction(
1897 executed_transaction: &ExecutedTransaction,
1898 expected_output_recipients: &[NoteRecipient],
1899) -> Result<(), ClientError> {
1900 let tx_output_recipient_digests = executed_transaction
1901 .output_notes()
1902 .iter()
1903 .filter_map(|n| n.recipient().map(NoteRecipient::digest))
1904 .collect::<Vec<_>>();
1905
1906 let missing_recipient_digest: Vec<Word> = expected_output_recipients
1907 .iter()
1908 .filter_map(|recipient| {
1909 (!tx_output_recipient_digests.contains(&recipient.digest()))
1910 .then_some(recipient.digest())
1911 })
1912 .collect();
1913
1914 if !missing_recipient_digest.is_empty() {
1915 return Err(ClientError::MissingOutputRecipients(missing_recipient_digest));
1916 }
1917
1918 Ok(())
1919}
1920
1921fn ensure_account_allowed(
1926 account_id: AccountId,
1927 is_allowed: Result<bool, ClientError>,
1928) -> Result<(), ClientError> {
1929 match is_allowed {
1930 Ok(true) => Ok(()),
1931 Ok(false) => Err(ClientError::AccountNotAllowlisted(account_id)),
1932 Err(err) => {
1933 info!(
1934 "could not check whether account {account_id} is on the network allowlist, \
1935 submitting anyway and letting the node decide: {err}"
1936 );
1937 Ok(())
1938 },
1939 }
1940}
1941
1942#[cfg(test)]
1946mod tests {
1947 use alloc::vec;
1948
1949 use miden_protocol::Word;
1950 use miden_protocol::account::auth::AuthSecretKey;
1951 use miden_protocol::account::{
1952 Account,
1953 AccountBuilder,
1954 AccountComponent,
1955 AccountComponentMetadata,
1956 AccountId,
1957 AccountType,
1958 };
1959 use miden_protocol::asset::{AssetId, FungibleAsset};
1960 use miden_protocol::block::{BlockHeader, BlockNumber, FeeParameters};
1961 use miden_protocol::crypto::SequentialCommit;
1962 use miden_protocol::note::{Note, NoteType};
1963 use miden_protocol::protocol_config::ProtocolConfig;
1964 use miden_protocol::testing::account_id::{
1965 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
1966 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
1967 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
1968 ACCOUNT_ID_SENDER,
1969 };
1970 use miden_standards::account::AccountBuilderSchemaCommitmentExt;
1971 use miden_standards::account::auth::{
1972 Approver,
1973 ApproverSet,
1974 AuthGuardedMultisig,
1975 AuthGuardedMultisigConfig,
1976 AuthMultisig,
1977 AuthMultisigConfig,
1978 AuthMultisigSmart,
1979 AuthMultisigSmartConfig,
1980 AuthSingleSig,
1981 FeeConversionInfo,
1982 GuardianConfig,
1983 MultisigAuthArgs,
1984 NoAuth,
1985 commit_fee_conversion_info,
1986 };
1987 use miden_standards::account::wallets::BasicWallet;
1988 use miden_standards::note::P2idNote;
1989 use rand::SeedableRng;
1990 use rand_chacha::ChaCha20Rng;
1991
1992 use super::{
1993 AccountComponentInterface,
1994 NATIVE_FEE_CONVERSION_SALT,
1995 TransactionRequest,
1996 TransactionRequestBuilder,
1997 attach_default_auth_args,
1998 validate_output_note_senders,
1999 };
2000 use crate::ClientError;
2001 use crate::assembly::CodeBuilder;
2002 use crate::auth::AuthSchemeId;
2003 use crate::rng::draw_word;
2004 use crate::transaction::TransactionRequestError;
2005
2006 fn own_note_with_sender(sender: AccountId) -> Note {
2007 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
2008 let target_id =
2009 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
2010 let mut rng = ChaCha20Rng::seed_from_u64(0);
2011
2012 P2idNote::builder()
2013 .sender(sender)
2014 .target(target_id)
2015 .asset(FungibleAsset::new(faucet_id, 100).unwrap())
2016 .note_type(NoteType::Public)
2017 .serial_number(draw_word(&mut rng))
2018 .build()
2019 .expect("note creation failed")
2020 .into()
2021 }
2022
2023 #[test]
2024 fn output_note_with_foreign_sender_is_rejected() {
2025 let account_id =
2026 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
2027 let foreign_sender = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
2028 assert_ne!(account_id, foreign_sender);
2029
2030 let request = TransactionRequestBuilder::new()
2031 .own_output_notes(vec![own_note_with_sender(foreign_sender)])
2032 .build()
2033 .unwrap();
2034
2035 let err = validate_output_note_senders(&request, account_id).unwrap_err();
2036 match err {
2037 ClientError::TransactionRequestError(
2038 TransactionRequestError::OutputNoteSenderMismatch { expected, actual },
2039 ) => {
2040 assert_eq!(expected, account_id);
2041 assert_eq!(actual, foreign_sender);
2042 },
2043 other => panic!("expected OutputNoteSenderMismatch, got {other:?}"),
2044 }
2045 }
2046
2047 #[test]
2048 fn output_note_with_matching_sender_is_accepted() {
2049 let account_id =
2050 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
2051
2052 let request = TransactionRequestBuilder::new()
2053 .own_output_notes(vec![own_note_with_sender(account_id)])
2054 .build()
2055 .unwrap();
2056
2057 validate_output_note_senders(&request, account_id).unwrap();
2058 }
2059
2060 #[test]
2061 fn request_without_own_output_notes_is_accepted() {
2062 let account_id =
2063 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
2064 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
2065
2066 let request = TransactionRequestBuilder::new()
2068 .input_notes(vec![(own_note_with_sender(faucet_id), None)])
2069 .build()
2070 .unwrap();
2071
2072 validate_output_note_senders(&request, account_id).unwrap();
2073 }
2074
2075 fn account_with_auth(auth_component: impl Into<AccountComponent>) -> Account {
2077 AccountBuilder::new([7u8; 32])
2078 .account_type(AccountType::Public)
2079 .with_component(auth_component)
2080 .with_component(BasicWallet)
2081 .build_with_schema_commitment()
2082 .expect("account creation failed")
2083 }
2084
2085 fn fee_conversion_request() -> TransactionRequest {
2086 TransactionRequestBuilder::new()
2087 .fee_conversion_salt(Word::from([13u32, 14, 15, 16]))
2088 .build()
2089 .unwrap()
2090 }
2091
2092 #[test]
2093 fn fee_conversion_info_is_accepted_by_a_signature_authenticated_account() {
2094 let key = AuthSecretKey::new_falcon512_poseidon2();
2095 let auth = AuthSingleSig::new(Approver::new(
2096 key.public_key().to_commitment(),
2097 AuthSchemeId::Falcon512Poseidon2,
2098 ));
2099
2100 try_injected_auth_arg(fee_conversion_request(), &account_with_auth(auth), 500).unwrap();
2101 }
2102
2103 #[test]
2104 fn fee_conversion_info_is_rejected_by_an_account_that_cannot_read_it() {
2105 let account = account_with_auth(NoAuth);
2106
2107 let err = try_injected_auth_arg(fee_conversion_request(), &account, 500)
2108 .expect_err("NoAuth does not read the auth args");
2109 match err {
2110 ClientError::TransactionRequestError(
2111 TransactionRequestError::FeeConversionInfoUnsupported(auth_component),
2112 ) => assert_eq!(auth_component, AccountComponentInterface::AuthNoAuth.name()),
2113 other => panic!("expected FeeConversionInfoUnsupported, got {other:?}"),
2114 }
2115 }
2116
2117 #[test]
2118 fn a_request_without_fee_conversion_info_skips_the_auth_component_check() {
2119 try_injected_auth_arg(
2121 TransactionRequestBuilder::new().build().unwrap(),
2122 &account_with_auth(NoAuth),
2123 500,
2124 )
2125 .unwrap();
2126 }
2127
2128 const NATIVE_FEE_FAUCET: u128 = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET;
2134
2135 fn test_protocol_config() -> ProtocolConfig {
2136 ProtocolConfig::current(AssetId::new_fungible(NATIVE_FEE_FAUCET.try_into().unwrap()))
2137 .unwrap()
2138 }
2139
2140 fn header_with_base_fee(verification_base_fee: u32) -> BlockHeader {
2142 let fee_parameters = FeeParameters::new(verification_base_fee);
2143 let (_, validator_keys) = miden_protocol::block::ValidatorConfig::random_with_signers(1);
2144
2145 BlockHeader::new(
2146 Word::empty(),
2147 BlockNumber::from(1u32),
2148 Word::empty(),
2149 Word::empty(),
2150 Word::empty(),
2151 Word::empty(),
2152 Word::empty(),
2153 validator_keys,
2154 fee_parameters,
2155 test_protocol_config().to_commitment(),
2156 None,
2157 0,
2158 )
2159 }
2160
2161 fn injected_auth_arg(
2164 mut request: TransactionRequest,
2165 account: &Account,
2166 verification_base_fee: u32,
2167 ) -> Option<Word> {
2168 let _ = attach_default_auth_args(
2169 &mut request,
2170 &account.code_interface(),
2171 &header_with_base_fee(verification_base_fee),
2172 &test_protocol_config(),
2173 );
2174 *request.auth_arg()
2175 }
2176
2177 fn try_injected_auth_arg(
2179 mut request: TransactionRequest,
2180 account: &Account,
2181 verification_base_fee: u32,
2182 ) -> Result<Option<Word>, ClientError> {
2183 attach_default_auth_args(
2184 &mut request,
2185 &account.code_interface(),
2186 &header_with_base_fee(verification_base_fee),
2187 &test_protocol_config(),
2188 )?;
2189 Ok(*request.auth_arg())
2190 }
2191
2192 fn singlesig_account() -> Account {
2193 let key = AuthSecretKey::new_falcon512_poseidon2();
2194 account_with_auth(AuthSingleSig::new(Approver::new(
2195 key.public_key().to_commitment(),
2196 AuthSchemeId::Falcon512Poseidon2,
2197 )))
2198 }
2199
2200 fn guarded_multisig_account() -> Account {
2201 let approvers = ApproverSet::new(
2202 vec![Approver::new(
2203 AuthSecretKey::new_falcon512_poseidon2().public_key().to_commitment(),
2204 AuthSchemeId::Falcon512Poseidon2,
2205 )],
2206 1,
2207 )
2208 .unwrap();
2209
2210 let guardian = GuardianConfig::new(Approver::new(
2211 AuthSecretKey::new_falcon512_poseidon2().public_key().to_commitment(),
2212 AuthSchemeId::Falcon512Poseidon2,
2213 ));
2214
2215 account_with_auth(
2216 AuthGuardedMultisig::new(AuthGuardedMultisigConfig::new(approvers, guardian).unwrap())
2217 .unwrap(),
2218 )
2219 }
2220
2221 #[test]
2222 fn native_fee_conversion_info_is_attached_on_a_fee_charging_chain() {
2223 let auth_arg = injected_auth_arg(
2224 TransactionRequestBuilder::new().build().unwrap(),
2225 &singlesig_account(),
2226 500,
2227 )
2228 .expect("a fee-charging chain should get conversion info attached");
2229
2230 let (expected, _) = commit_fee_conversion_info(
2231 FeeConversionInfo::one_to_one(AccountId::try_from(NATIVE_FEE_FAUCET).unwrap()),
2232 NATIVE_FEE_CONVERSION_SALT,
2233 );
2234 assert_eq!(auth_arg, expected, "the fee should be paid in the native asset at rate 1/1");
2235 }
2236
2237 #[test]
2238 #[allow(deprecated)]
2239 fn an_explicit_auth_arg_is_not_overwritten() {
2240 let auth_arg = Word::from([21u32, 22, 23, 24]);
2241 let request = TransactionRequestBuilder::new().auth_arg(auth_arg).build().unwrap();
2242
2243 assert_eq!(
2244 injected_auth_arg(request, &singlesig_account(), 500),
2245 Some(auth_arg),
2246 "a request that declares its own auth arg keeps it"
2247 );
2248 }
2249
2250 fn native_commitment(salt: Word) -> Word {
2252 let (auth_arg, _) = commit_fee_conversion_info(
2253 FeeConversionInfo::one_to_one(AccountId::try_from(NATIVE_FEE_FAUCET).unwrap()),
2254 salt,
2255 );
2256 auth_arg
2257 }
2258
2259 fn native_multisig_commitment(salt: Word) -> Word {
2260 MultisigAuthArgs::new(BlockNumber::from(1u32), salt)
2261 .with_conversion_info(FeeConversionInfo::one_to_one(
2262 AccountId::try_from(NATIVE_FEE_FAUCET).unwrap(),
2263 ))
2264 .to_commitment()
2265 }
2266
2267 #[test]
2268 fn a_declared_salt_is_used_for_the_native_commitment() {
2269 let salt = Word::from([17u32, 18, 19, 20]);
2270 let request = TransactionRequestBuilder::new().fee_conversion_salt(salt).build().unwrap();
2271
2272 let auth_arg = injected_auth_arg(request, &singlesig_account(), 500)
2273 .expect("a declared salt should still get native conversion info attached");
2274
2275 assert_eq!(auth_arg, native_commitment(salt));
2276 }
2277
2278 fn multisig_account() -> Account {
2279 let approvers = ApproverSet::new(
2280 vec![Approver::new(
2281 AuthSecretKey::new_falcon512_poseidon2().public_key().to_commitment(),
2282 AuthSchemeId::Falcon512Poseidon2,
2283 )],
2284 1,
2285 )
2286 .unwrap();
2287
2288 account_with_auth(AuthMultisig::new(AuthMultisigConfig::new(approvers)).unwrap())
2289 }
2290
2291 #[test]
2292 fn nothing_is_attached_where_it_is_not_needed_or_not_readable() {
2293 for (case, account, base_fee) in [
2294 ("a zero base fee charges nothing", singlesig_account(), 0),
2295 ("NoAuth never reads the auth args", account_with_auth(NoAuth), 500),
2296 ("a multisig salt is its own replay guard", multisig_account(), 500),
2297 ] {
2298 assert_eq!(
2299 injected_auth_arg(
2300 TransactionRequestBuilder::new().build().unwrap(),
2301 &account,
2302 base_fee
2303 ),
2304 None,
2305 "{case}"
2306 );
2307 }
2308 }
2309
2310 #[test]
2317 fn fee_conversion_info_is_accepted_by_a_guarded_multisig_account() {
2318 try_injected_auth_arg(fee_conversion_request(), &guarded_multisig_account(), 500)
2319 .expect("a guarded multisig reads the auth args as conversion info");
2320 }
2321
2322 #[test]
2324 fn a_guarded_multisig_account_must_declare_its_own_fee_conversion_info() {
2325 let err = try_injected_auth_arg(
2326 TransactionRequestBuilder::new().build().unwrap(),
2327 &guarded_multisig_account(),
2328 500,
2329 )
2330 .expect_err("a guarded multisig account cannot inherit the fixed native salt");
2331 match err {
2332 ClientError::TransactionRequestError(
2333 TransactionRequestError::FeeConversionInfoRequired(auth_component),
2334 ) => {
2335 assert_eq!(auth_component, AccountComponentInterface::AuthGuardedMultisig.name());
2336 },
2337 other => panic!("expected FeeConversionInfoRequired, got {other:?}"),
2338 }
2339
2340 let salt = Word::from([13u32, 14, 15, 16]);
2341 assert_eq!(
2342 try_injected_auth_arg(fee_conversion_request(), &guarded_multisig_account(), 500)
2343 .expect("a declared salt is accepted"),
2344 Some(native_multisig_commitment(salt)),
2345 "a guarded multisig account that declares a salt commits the native conversion info"
2346 );
2347 }
2348
2349 fn smart_multisig_account() -> Account {
2353 let approvers = ApproverSet::new(
2354 vec![Approver::new(
2355 AuthSecretKey::new_falcon512_poseidon2().public_key().to_commitment(),
2356 AuthSchemeId::Falcon512Poseidon2,
2357 )],
2358 1,
2359 )
2360 .unwrap();
2361
2362 account_with_auth(AuthMultisigSmart::new(AuthMultisigSmartConfig::new(approvers)).unwrap())
2363 }
2364
2365 #[test]
2367 fn a_multisig_account_must_declare_a_salt_on_a_chain_without_fees() {
2368 let err = try_injected_auth_arg(
2369 TransactionRequestBuilder::new().build().unwrap(),
2370 &guarded_multisig_account(),
2371 0,
2372 )
2373 .expect_err("a multisig account always reads auth args");
2374 assert!(matches!(
2375 err,
2376 ClientError::TransactionRequestError(
2377 TransactionRequestError::FeeConversionInfoRequired(_)
2378 )
2379 ));
2380 }
2381
2382 #[test]
2384 fn fee_conversion_info_is_accepted_by_a_smart_multisig_account() {
2385 try_injected_auth_arg(fee_conversion_request(), &smart_multisig_account(), 500)
2386 .expect("a smart multisig reads the auth args as conversion info");
2387 }
2388
2389 #[test]
2391 fn a_smart_multisig_account_must_declare_its_own_fee_conversion_info() {
2392 let err = try_injected_auth_arg(
2393 TransactionRequestBuilder::new().build().unwrap(),
2394 &smart_multisig_account(),
2395 500,
2396 )
2397 .expect_err("a smart multisig account cannot inherit the fixed native salt");
2398 match err {
2399 ClientError::TransactionRequestError(
2400 TransactionRequestError::FeeConversionInfoRequired(auth_component),
2401 ) => {
2402 assert_eq!(auth_component, AccountComponentInterface::AuthMultisigSmart.name());
2403 },
2404 other => panic!("expected FeeConversionInfoRequired, got {other:?}"),
2405 }
2406
2407 let salt = Word::from([13u32, 14, 15, 16]);
2408 assert_eq!(
2409 try_injected_auth_arg(fee_conversion_request(), &smart_multisig_account(), 500)
2410 .expect("a declared salt is accepted"),
2411 Some(native_multisig_commitment(salt)),
2412 "a smart multisig account that declares a salt commits the native conversion info"
2413 );
2414 }
2415
2416 #[test]
2419 fn an_unrecognized_auth_component_is_rejected_rather_than_panicking() {
2420 const CUSTOM_AUTH: &str = "
2421 use miden::protocol::native_account
2422
2423 @auth_script
2424 pub proc auth_custom
2425 exec.native_account::incr_nonce
2426 drop
2427 end
2428 ";
2429
2430 let code = CodeBuilder::default()
2431 .compile_component_code("miden::testing::custom_auth", CUSTOM_AUTH)
2432 .expect("custom auth component code should compile");
2433 let auth = AccountComponent::new(
2434 code,
2435 vec![],
2436 AccountComponentMetadata::new("miden::testing::custom_auth"),
2437 )
2438 .expect("custom auth component");
2439
2440 let account = account_with_auth(auth);
2441
2442 let err = try_injected_auth_arg(fee_conversion_request(), &account, 500)
2443 .expect_err("an account with no recognized auth component cannot read conversion info");
2444 assert!(matches!(
2445 err,
2446 ClientError::TransactionRequestError(
2447 TransactionRequestError::FeeConversionInfoUnsupported(_)
2448 )
2449 ));
2450
2451 assert_eq!(
2452 try_injected_auth_arg(TransactionRequestBuilder::new().build().unwrap(), &account, 500)
2453 .expect("a request declaring nothing is left alone"),
2454 None,
2455 "an auth component nothing can reason about gets nothing attached"
2456 );
2457 }
2458}