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::{AccountFetch, GetAccountRequest};
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();
210
211impl<AUTH> Client<AUTH>
213where
214 AUTH: TransactionAuthenticator + Sync + 'static,
215{
216 pub async fn get_transactions(
221 &self,
222 filter: TransactionFilter,
223 ) -> Result<Vec<TransactionRecord>, ClientError> {
224 self.store.get_transactions(filter).await.map_err(Into::into)
225 }
226
227 pub async fn submit_new_transaction(
235 &mut self,
236 account_id: AccountId,
237 transaction_request: TransactionRequest,
238 ) -> Result<TransactionId, ClientError> {
239 let prover = self.tx_prover.clone();
240 self.submit_new_transaction_with_prover(account_id, transaction_request, prover)
241 .await
242 }
243
244 pub async fn submit_new_transaction_with_prover(
250 &mut self,
251 account_id: AccountId,
252 transaction_request: TransactionRequest,
253 tx_prover: Arc<dyn TransactionProver>,
254 ) -> Result<TransactionId, ClientError> {
255 if !transaction_request.expected_ntx_scripts().is_empty() {
258 Box::pin(self.ensure_ntx_scripts_registered(
259 account_id,
260 transaction_request.expected_ntx_scripts(),
261 tx_prover.clone(),
262 ))
263 .await?;
264 }
265
266 let tx_result = self.execute_transaction(account_id, transaction_request).await?;
267 let tx_id = tx_result.executed_transaction().id();
268
269 let proven_transaction = self.prove_transaction_with(&tx_result, tx_prover).await?;
270 let submission_height =
271 self.submit_proven_transaction(proven_transaction, &tx_result).await?;
272
273 let tx_update =
281 Box::new(self.get_transaction_store_update(&tx_result, submission_height).await?);
282
283 if let Err(apply_err) = self.apply_transaction_update((*tx_update).clone()).await {
284 info!(
285 "apply_transaction_update failed for submitted tx {tx_id}; returning \
286 ApplyTransactionAfterSubmitFailed with the pending update attached: {apply_err}"
287 );
288 return Err(ClientError::ApplyTransactionAfterSubmitFailed {
289 pending_update: tx_update,
290 source: Box::new(apply_err),
291 });
292 }
293
294 for observer in &self.transaction_observers {
298 crate::errors::log_observer_failure(
299 observer.name(),
300 "TransactionObserver::apply",
301 observer.apply(&tx_result).await,
302 );
303 }
304
305 Ok(tx_id)
306 }
307
308 pub async fn execute_transaction(
318 &self,
319 account_id: AccountId,
320 transaction_request: TransactionRequest,
321 ) -> Result<TransactionResult, ClientError> {
322 Box::pin(self.execute_transaction_with_mode(
323 account_id,
324 transaction_request,
325 TransactionExecutionMode::Standard,
326 None,
327 ))
328 .await
329 }
330
331 pub async fn execute_transaction_at(
365 &mut self,
366 account_id: AccountId,
367 transaction_request: TransactionRequest,
368 anchor: ChainAnchor,
369 ) -> Result<TransactionResult, ClientError> {
370 let result = self
371 .execute_transaction_with_mode(
372 account_id,
373 transaction_request,
374 TransactionExecutionMode::Standard,
375 Some(Box::new(anchor)),
376 )
377 .await?;
378
379 let expiration = result.executed_transaction().expiration_block_num();
384 let sync_height = self.store.get_sync_height().await?;
385 if expiration <= sync_height {
386 return Err(
387 ChainAnchorError::AnchoredTransactionExpired { expiration, sync_height }.into()
388 );
389 }
390
391 Ok(result)
392 }
393
394 async fn chain_anchor_at_tip(
399 &self,
400 tracked_blocks: BTreeSet<BlockNumber>,
401 ) -> Result<ChainAnchor, ClientError> {
402 let sync_height = self.store.get_sync_height().await?;
403
404 let (header, _had_notes) = self
405 .store
406 .get_block_header_by_num(sync_height)
407 .await?
408 .ok_or(StoreError::BlockHeaderNotFound(sync_height))?;
409
410 let mut tracked_blocks = tracked_blocks;
411 tracked_blocks.remove(&sync_height);
413 if let Some(&future_block) =
414 tracked_blocks.iter().find(|&&block_num| block_num > sync_height)
415 {
416 return Err(StoreError::BlockHeaderNotFound(future_block).into());
417 }
418
419 let peaks = self.store.get_current_blockchain_peaks().await?;
420 let (partial_mmr, block_headers) = build_partial_mmr_and_headers_with_fallback(
421 &self.store,
422 &self.rpc_api,
423 peaks,
424 &tracked_blocks,
425 )
426 .await?;
427
428 let chain = PartialBlockchain::new(partial_mmr, block_headers)?;
429
430 Ok(ChainAnchor::new(header, chain)?)
431 }
432
433 pub async fn chain_anchor_for_request(
452 &self,
453 transaction_request: &TransactionRequest,
454 ) -> Result<ChainAnchor, ClientError> {
455 let inferred_input_note_ids: Vec<NoteId> = transaction_request
456 .input_note_ids()
457 .filter(|note_id| !transaction_request.explicit_input_notes.contains_key(note_id))
458 .collect();
459
460 let mut tracked_blocks: BTreeSet<BlockNumber> = if inferred_input_note_ids.is_empty() {
461 BTreeSet::new()
462 } else {
463 self.store
464 .get_input_notes(NoteFilter::List(inferred_input_note_ids))
465 .await?
466 .iter()
467 .filter(|record| record.is_authenticated())
468 .filter_map(|record| record.inclusion_proof())
469 .map(|proof| proof.location().block_num())
470 .collect()
471 };
472 tracked_blocks.extend(
473 transaction_request
474 .explicit_input_notes
475 .values()
476 .filter_map(InputNote::proof)
477 .map(|proof| proof.location().block_num()),
478 );
479 tracked_blocks.extend(transaction_request.block_numbers().iter().copied());
480
481 self.chain_anchor_at_tip(tracked_blocks).await
482 }
483
484 #[cfg(feature = "dap")]
498 pub async fn execute_transaction_with_dap(
499 &self,
500 account_id: AccountId,
501 transaction_request: TransactionRequest,
502 ) -> Result<TransactionResult, ClientError> {
503 self.execute_transaction_with_mode(
504 account_id,
505 transaction_request,
506 TransactionExecutionMode::Dap,
507 None,
508 )
509 .await
510 }
511
512 async fn execute_transaction_with_mode(
516 &self,
517 account_id: AccountId,
518 transaction_request: TransactionRequest,
519 execution_mode: TransactionExecutionMode,
520 anchor: Option<Box<ChainAnchor>>,
521 ) -> Result<TransactionResult, ClientError> {
522 let account: PartialAccount =
523 self.get_native_account_record(account_id).await?.try_into()?;
524
525 let prep = self
526 .prepare_transaction(&account, transaction_request, anchor.as_deref())
527 .await?;
528
529 let mut data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
530 if let Some(anchor) = anchor {
531 data_store = data_store.with_chain_anchor(*anchor);
532 }
533 data_store.register_note_scripts(prep.output_note_scripts());
534 data_store.register_block_numbers(prep.block_numbers.iter().copied());
535 for fpi_account in &prep.foreign_account_inputs {
536 data_store.mast_store().load_account_code(fpi_account.code());
537 }
538 data_store.register_foreign_account_inputs(prep.foreign_account_inputs);
539
540 data_store.mast_store().load_account_code(account.code());
541
542 let mut notes = prep.notes;
543 if prep.ignore_invalid_notes {
544 notes = self
545 .get_valid_input_notes(
546 &data_store,
547 account.id(),
548 prep.block_num,
549 notes,
550 prep.tx_args.clone(),
551 )
552 .await?;
553 }
554
555 let executed_transaction = match execution_mode {
556 TransactionExecutionMode::Standard => {
557 self.build_executor(&data_store)?
558 .execute_transaction(account_id, prep.block_num, notes, prep.tx_args)
559 .await?
560 },
561 #[cfg(feature = "dap")]
562 TransactionExecutionMode::Dap => {
563 self.build_dap_executor(&data_store)?
564 .execute_transaction(account_id, prep.block_num, notes, prep.tx_args)
565 .await?
566 },
567 };
568
569 validate_executed_transaction(&executed_transaction, &prep.output_recipients)?;
570 TransactionResult::new(executed_transaction, prep.future_notes)
571 }
572
573 pub(crate) async fn prepare_transaction(
589 &self,
590 account: &PartialAccount,
591 transaction_request: TransactionRequest,
592 anchor: Option<&ChainAnchor>,
593 ) -> Result<PreparedTransaction, ClientError> {
594 self.validate_account_request(
595 &transaction_request,
596 account.id(),
597 &account.code_interface(),
598 )
599 .await?;
600
601 self.prepare_transaction_inner(account.code_interface(), transaction_request, anchor)
602 .await
603 }
604
605 pub(crate) async fn prepare_transaction_for_batch(
606 &self,
607 account: &PartialAccount,
608 transaction_request: TransactionRequest,
609 ) -> Result<PreparedTransaction, ClientError> {
610 self.prepare_transaction_inner(account.code_interface(), transaction_request, None)
611 .await
612 }
613
614 async fn prepare_transaction_inner(
615 &self,
616 account_code_interface: AccountCodeInterface,
617 mut transaction_request: TransactionRequest,
618 anchor: Option<&ChainAnchor>,
619 ) -> Result<PreparedTransaction, ClientError> {
620 if anchor.is_none() {
621 self.validate_recency().await?;
622 }
623
624 let mut stored_note_records = self
626 .store
627 .get_input_notes(NoteFilter::List(transaction_request.input_note_ids().collect()))
628 .await?;
629
630 for note in &stored_note_records {
635 if note.is_consumed() {
636 return Err(ClientError::TransactionRequestError(
637 TransactionRequestError::InputNoteAlreadyConsumed(note.details_commitment()),
638 ));
639 }
640 if let Some(transaction_id) = note.consumer_transaction_id()
641 && note.is_processing()
642 {
643 return Err(ClientError::TransactionRequestError(
644 TransactionRequestError::InputNoteBeingProcessed {
645 note: note.details_commitment(),
646 transaction_id: *transaction_id,
647 },
648 ));
649 }
650 }
651
652 stored_note_records.retain(InputNoteRecord::is_authenticated);
654
655 let notes = transaction_request.build_input_notes(stored_note_records)?;
656
657 if let Some(anchor) = anchor {
661 for note in notes.iter() {
662 if let Some(location) = note.location() {
663 let block_num = location.block_num();
664 if block_num < anchor.block_num()
665 && !anchor.partial_blockchain().contains_block(block_num)
666 {
667 return Err(ChainAnchorError::BlockNotTracked { block_num }.into());
668 }
669 }
670 }
671 }
672
673 let output_recipients =
674 transaction_request.expected_output_recipients().cloned().collect::<Vec<_>>();
675
676 let future_notes: Vec<(NoteDetails, NoteTag)> =
677 transaction_request.expected_future_notes().cloned().collect();
678
679 let tx_script = transaction_request.build_transaction_script(&account_code_interface)?;
680
681 let foreign_accounts = transaction_request.foreign_accounts().clone();
682
683 let block_num = match anchor {
686 Some(anchor) => anchor.block_num(),
687 None => self.store.get_sync_height().await?,
688 };
689
690 let foreign_account_inputs = self
691 .get_foreign_account_inputs(foreign_accounts.into_values(), block_num)
692 .await?;
693
694 let ignore_invalid_notes = transaction_request.ignore_invalid_input_notes();
695 let mut block_numbers = transaction_request.block_numbers().clone();
696 block_numbers.extend(
697 AuthArgsLayout::of(&account_code_interface)
698 .bound_block(&transaction_request)
699 .filter(|bound_block| *bound_block < block_num),
700 );
701
702 let reference_header = match anchor {
703 Some(anchor) => anchor.header().clone(),
704 None => {
705 self.store
706 .get_block_header_by_num(block_num)
707 .await?
708 .ok_or(StoreError::BlockHeaderNotFound(block_num))?
709 .0
710 },
711 };
712
713 for inputs in &foreign_account_inputs {
716 if inputs.compute_account_root().ok() != Some(reference_header.account_root()) {
717 return Err(TransactionRequestError::ForeignAccountNotAtReferenceBlock {
718 account_id: inputs.id(),
719 block_num,
720 }
721 .into());
722 }
723 }
724
725 attach_default_auth_args(
726 &mut transaction_request,
727 &account_code_interface,
728 &reference_header,
729 &self.get_protocol_config(reference_header.protocol_config_commitment()).await?,
730 )?;
731
732 let tx_args = transaction_request.into_transaction_args(tx_script);
733
734 Ok(PreparedTransaction {
735 notes,
736 output_recipients,
737 future_notes,
738 tx_args,
739 foreign_account_inputs,
740 block_numbers,
741 block_num,
742 ignore_invalid_notes,
743 })
744 }
745
746 pub async fn prove_transaction(
748 &self,
749 tx_result: &TransactionResult,
750 ) -> Result<ProvenTransaction, ClientError> {
751 self.prove_transaction_with(tx_result, self.tx_prover.clone()).await
752 }
753
754 pub async fn prove_transaction_with(
762 &self,
763 tx_result: &TransactionResult,
764 tx_prover: Arc<dyn TransactionProver>,
765 ) -> Result<ProvenTransaction, ClientError> {
766 info!("Proving transaction...");
767
768 let executed_transaction = tx_result.executed_transaction();
769 let proven_transaction = tx_prover.prove(executed_transaction.clone().into()).await?;
770
771 if proven_transaction.id() != executed_transaction.id() {
780 return Err(ClientError::MismatchedProvenTransaction {
781 requested: executed_transaction.id(),
782 returned: proven_transaction.id(),
783 });
784 }
785
786 info!("Transaction proven.");
787
788 Ok(proven_transaction)
789 }
790
791 pub async fn submit_proven_transaction(
801 &mut self,
802 proven_transaction: ProvenTransaction,
803 transaction_inputs: impl Into<TransactionInputs>,
804 ) -> Result<BlockNumber, ClientError> {
805 let account_id = proven_transaction.account_id();
807 if self.is_allowlist_gated(account_id).await? {
808 ensure_account_allowed(account_id, self.is_account_allowed(account_id).await)?;
809 }
810
811 info!("Submitting transaction to the network...");
812 let tx_id = proven_transaction.id();
813 let key = self.transaction_encryption_key().await?;
814
815 let transaction_inputs = transaction_inputs.into();
819
820 let sealed_inputs =
821 seal_transaction_inputs(&mut self.rng, &key, tx_id, &transaction_inputs)?;
822
823 let result =
824 self.rpc_api.submit_proven_transaction(&proven_transaction, sealed_inputs).await;
825 if let Err(err) = &result {
826 self.forget_stale_transaction_encryption_key(err).await;
827 }
828
829 let block_num = result.map_err(|err| {
830 promote_indeterminate_submission(err, proven_transaction, transaction_inputs)
831 })?;
832 info!("Transaction submitted.");
833
834 Ok(block_num)
835 }
836
837 pub(crate) async fn transaction_encryption_key(
845 &self,
846 ) -> Result<TransactionEncryptionKey, ClientError> {
847 if let Some(key) = self.store.get_transaction_encryption_key().await? {
848 return Ok(key);
849 }
850
851 let attested = self.rpc_api.get_transaction_encryption_key().await?;
852
853 let genesis_commitment =
857 self.trusted_block_header(BlockNumber::GENESIS).await?.commitment();
858 let validator_keys = self.get_validator_config().await?;
859
860 let key = attested.verify(genesis_commitment, &validator_keys)?;
861 self.store.set_transaction_encryption_key(&key).await?;
862
863 Ok(key)
864 }
865
866 #[cfg(feature = "testing")]
869 pub async fn seed_transaction_encryption_key(
870 &self,
871 key: TransactionEncryptionKey,
872 ) -> Result<(), ClientError> {
873 Ok(self.store.set_transaction_encryption_key(&key).await?)
874 }
875
876 pub(crate) async fn forget_stale_transaction_encryption_key(&self, err: &RpcError) {
882 if err.is_stale_transaction_encryption_key()
883 && let Err(err) = self.store.remove_transaction_encryption_key().await
884 {
885 tracing::warn!("failed to evict the stale transaction encryption key: {err}");
886 }
887 }
888
889 async fn trusted_block_header(
896 &self,
897 block_num: BlockNumber,
898 ) -> Result<BlockHeader, ClientError> {
899 self.store.get_block_header_by_num(block_num).await?.map(|(header, _)| header).ok_or_else(
900 || {
901 ClientError::ChainValidationError(alloc::format!(
902 "block header {block_num} is not tracked locally; sync the client before it can verify data against the chain"
903 ))
904 },
905 )
906 }
907
908 pub async fn get_transaction_store_update(
911 &self,
912 tx_result: &TransactionResult,
913 submission_height: BlockNumber,
914 ) -> Result<TransactionStoreUpdate, TransactionStoreUpdateError> {
915 let note_updates = self.get_note_updates(submission_height, tx_result).await?;
916
917 let new_tags: Vec<NoteTagRecord> = note_updates
920 .updated_input_notes()
921 .filter_map(|note| {
922 let note = note.inner();
923
924 if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) =
925 note.state()
926 {
927 Some(NoteTagRecord::with_note_source(*tag, note.details_commitment()))
928 } else {
929 None
930 }
931 })
932 .collect();
933
934 Ok(TransactionStoreUpdate::new(
935 tx_result.executed_transaction().clone(),
936 submission_height,
937 note_updates,
938 tx_result.future_notes().to_vec(),
939 new_tags,
940 ))
941 }
942
943 pub async fn apply_transaction(
946 &self,
947 tx_result: &TransactionResult,
948 submission_height: BlockNumber,
949 ) -> Result<(), ClientError> {
950 let tx_update = self.get_transaction_store_update(tx_result, submission_height).await?;
951
952 self.apply_transaction_update(tx_update).await?;
953
954 for observer in &self.transaction_observers {
956 if let Err(err) = observer.apply(tx_result).await {
957 tracing::warn!(
958 observer = observer.name(),
959 error = ?err,
960 "TransactionObserver::apply failed; continuing with remaining observers",
961 );
962 }
963 }
964
965 Ok(())
966 }
967
968 pub async fn apply_transaction_update(
969 &self,
970 tx_update: TransactionStoreUpdate,
971 ) -> Result<(), ClientError> {
972 info!("Applying transaction to the local store...");
975
976 let executed_transaction = tx_update.executed_transaction();
977 let account_id = executed_transaction.account_id();
978
979 if self.account_reader(account_id).status().await?.is_locked() {
980 return Err(ClientError::AccountLocked(account_id));
981 }
982
983 self.store.apply_transaction(tx_update).await?;
984 info!("Transaction stored.");
985 Ok(())
986 }
987
988 pub async fn execute_program(
993 &self,
994 account_id: AccountId,
995 tx_script: TransactionScript,
996 advice_inputs: AdviceInputs,
997 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
998 ) -> Result<[Felt; MIN_STACK_DEPTH], ClientError> {
999 let (data_store, block_ref) =
1000 self.prepare_program_execution(account_id, foreign_accounts).await?;
1001
1002 Ok(self
1003 .build_executor(&data_store)?
1004 .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
1005 .await?)
1006 }
1007
1008 #[cfg(feature = "dap")]
1011 pub async fn execute_program_with_dap(
1012 &self,
1013 account_id: AccountId,
1014 tx_script: TransactionScript,
1015 advice_inputs: AdviceInputs,
1016 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
1017 ) -> Result<[Felt; MIN_STACK_DEPTH], ClientError> {
1018 let (data_store, block_ref) =
1019 self.prepare_program_execution(account_id, foreign_accounts).await?;
1020
1021 Ok(self
1022 .build_dap_executor(&data_store)?
1023 .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
1024 .await?)
1025 }
1026
1027 pub async fn validate_request(
1037 &self,
1038 account_id: AccountId,
1039 transaction_request: &TransactionRequest,
1040 ) -> Result<(), ClientError> {
1041 self.validate_recency().await?;
1042 validate_output_note_senders(transaction_request, account_id)?;
1043 let account: PartialAccount = self
1044 .store
1045 .get_minimal_partial_account(account_id)
1046 .await?
1047 .ok_or(ClientError::AccountDataNotFound(account_id))?
1048 .try_into()?;
1049 self.validate_account_request(transaction_request, account_id, &account.code_interface())
1050 .await
1051 }
1052
1053 async fn validate_account_request(
1058 &self,
1059 transaction_request: &TransactionRequest,
1060 account_id: AccountId,
1061 account_code_interface: &AccountCodeInterface,
1062 ) -> Result<(), ClientError> {
1063 if account_code_interface.contains([FungibleFaucet::mint_and_send_root()]) {
1064 Ok(())
1066 } else {
1067 let assets = self.account_reader(account_id).assets().await?;
1068 validate_basic_account_request(transaction_request, &assets)
1069 }
1070 }
1071
1072 async fn validate_recency(&self) -> Result<(), ClientError> {
1073 if let Some(max_block_number_delta) = self.max_block_number_delta {
1074 let current_chain_tip =
1075 self.rpc_api.get_block_header_by_number(None, false).await?.0.block_num();
1076
1077 if current_chain_tip > self.store.get_sync_height().await? + max_block_number_delta {
1078 return Err(ClientError::RecencyConditionError(
1079 "The client is too far behind the chain tip to execute the transaction",
1080 ));
1081 }
1082 }
1083 Ok(())
1084 }
1085
1086 pub async fn ensure_ntx_scripts_registered(
1099 &mut self,
1100 account_id: AccountId,
1101 scripts: &[NoteScript],
1102 tx_prover: Arc<dyn TransactionProver>,
1103 ) -> Result<(), ClientError> {
1104 let mut missing_scripts = Vec::new();
1105
1106 for script in scripts {
1107 if StandardNote::from_script(script).is_some() {
1109 continue;
1110 }
1111
1112 let script_root = script.root();
1113
1114 match self.rpc_api.get_note_script_by_root(script_root.into()).await {
1116 Ok(Some(_)) => {},
1117 Ok(None) => missing_scripts.push(script.clone()),
1118 Err(source) => {
1119 return Err(ClientError::NtxScriptRegistrationFailed {
1120 script_root: script_root.into(),
1121 source,
1122 });
1123 },
1124 }
1125 }
1126
1127 if missing_scripts.is_empty() {
1128 return Ok(());
1129 }
1130
1131 let registration_request = TransactionRequestBuilder::new().build_register_note_scripts(
1132 account_id,
1133 missing_scripts,
1134 self.rng(),
1135 )?;
1136
1137 let tx_result = self.execute_transaction(account_id, registration_request).await?;
1138 let proven = self.prove_transaction_with(&tx_result, tx_prover).await?;
1139 let submission_height = self.submit_proven_transaction(proven, &tx_result).await?;
1140 self.apply_transaction(&tx_result, submission_height).await?;
1141
1142 Ok(())
1143 }
1144
1145 pub(crate) async fn get_valid_input_notes<STORE: DataStore + Sync>(
1154 &self,
1155 data_store: &STORE,
1156 account_id: AccountId,
1157 block_ref: BlockNumber,
1158 mut input_notes: InputNotes<InputNote>,
1159 tx_args: TransactionArgs,
1160 ) -> Result<InputNotes<InputNote>, ClientError> {
1161 loop {
1162 if input_notes.is_empty() {
1165 break;
1166 }
1167
1168 let execution = NoteConsumptionChecker::new(&self.build_executor(data_store)?)
1169 .check_notes_consumability(
1170 account_id,
1171 block_ref,
1172 input_notes.iter().map(|n| n.clone().into_note()).collect(),
1173 tx_args.clone(),
1174 )
1175 .await?;
1176
1177 if execution.failed().is_empty() {
1178 break;
1179 }
1180
1181 let failed_note_ids: BTreeSet<NoteId> =
1182 execution.failed().iter().map(|n| n.note().id()).collect();
1183 let filtered_input_notes = InputNotes::new(
1184 input_notes
1185 .into_iter()
1186 .filter(|note| !failed_note_ids.contains(¬e.id()))
1187 .collect(),
1188 )
1189 .expect("Created from a valid input notes list");
1190
1191 input_notes = filtered_input_notes;
1192 }
1193
1194 Ok(input_notes)
1195 }
1196
1197 pub async fn get_foreign_account_inputs(
1219 &self,
1220 foreign_accounts: impl IntoIterator<Item = ForeignAccount>,
1221 block_num: BlockNumber,
1222 ) -> Result<Vec<AccountInputs>, ClientError> {
1223 let foreign_accounts = foreign_accounts.into_iter();
1224 let mut return_foreign_account_inputs = Vec::with_capacity(foreign_accounts.size_hint().0);
1225
1226 for foreign_account in foreign_accounts {
1227 let foreign_account_inputs = match foreign_account {
1228 ForeignAccount::Public(account_id, fetch) => {
1229 fetch_public_account_inputs(
1230 &self.store,
1231 &self.rpc_api,
1232 account_id,
1233 fetch,
1234 AccountStateAt::Block(block_num),
1235 )
1236 .await?
1237 },
1238 ForeignAccount::Private(partial_account) => {
1239 let witness =
1242 self.get_account_witness_at(partial_account.id(), block_num).await?;
1243
1244 AccountInputs::new(partial_account, witness)
1245 },
1246 ForeignAccount::Prefetched(inputs) => inputs,
1247 };
1248
1249 return_foreign_account_inputs.push(foreign_account_inputs);
1250 }
1251
1252 Ok(return_foreign_account_inputs)
1253 }
1254
1255 async fn get_account_witness_at(
1262 &self,
1263 account_id: AccountId,
1264 block_num: BlockNumber,
1265 ) -> Result<AccountWitness, ClientError> {
1266 if block_num == self.store.get_sync_height().await?
1268 && let Some(witness) = self.store.get_account_witness(account_id).await?
1269 {
1270 return Ok(witness);
1271 }
1272
1273 let (_, account_proof) = self
1274 .rpc_api
1275 .get_account(GetAccountRequest::new(account_id).at(AccountStateAt::Block(block_num)))
1276 .await?;
1277
1278 Ok(account_proof.into_parts().0)
1279 }
1280
1281 async fn prepare_program_execution(
1285 &self,
1286 account_id: AccountId,
1287 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
1288 ) -> Result<(ClientDataStore, BlockNumber), ClientError> {
1289 let block_ref = self.get_sync_height().await?;
1290
1291 let foreign_account_inputs = self
1292 .get_foreign_account_inputs(foreign_accounts.into_values(), block_ref)
1293 .await?;
1294
1295 let account_code = self
1296 .store
1297 .get_account_code(account_id)
1298 .await?
1299 .ok_or(ClientError::AccountDataNotFound(account_id))?;
1300
1301 let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
1302
1303 data_store.mast_store().load_account_code(&account_code);
1305
1306 for fpi_account in &foreign_account_inputs {
1307 data_store.mast_store().load_account_code(fpi_account.code());
1308 }
1309
1310 data_store.register_foreign_account_inputs(foreign_account_inputs);
1311
1312 Ok((data_store, block_ref))
1313 }
1314
1315 pub(crate) fn build_executor<'store, 'auth, STORE: DataStore + Sync>(
1318 &'auth self,
1319 data_store: &'store STORE,
1320 ) -> Result<TransactionExecutor<'store, 'auth, STORE, AUTH>, TransactionExecutorError> {
1321 let mut executor = TransactionExecutor::new(data_store)
1322 .with_options(self.exec_options)?
1323 .with_source_manager(self.source_manager.clone());
1324 if let Some(authenticator) = self.authenticator.as_deref() {
1325 executor = executor.with_authenticator(authenticator);
1326 }
1327 Ok(executor)
1328 }
1329
1330 async fn get_native_account_record(
1335 &self,
1336 account_id: AccountId,
1337 ) -> Result<AccountRecord, ClientError> {
1338 let account_record = self
1339 .store
1340 .get_minimal_partial_account(account_id)
1341 .await?
1342 .ok_or(ClientError::AccountDataNotFound(account_id))?;
1343 if account_record.is_watched() {
1344 return Err(ClientError::AccountIsWatched(account_id));
1345 }
1346 Ok(account_record)
1347 }
1348
1349 #[cfg(feature = "dap")]
1351 pub(crate) fn build_dap_executor<'store, 'auth, STORE: DataStore + Sync>(
1352 &'auth self,
1353 data_store: &'store STORE,
1354 ) -> Result<
1355 TransactionExecutor<'store, 'auth, STORE, AUTH, dap_executor::DapProgramExecutor>,
1356 TransactionExecutorError,
1357 > {
1358 Ok(self
1359 .build_executor(data_store)?
1360 .with_program_executor::<dap_executor::DapProgramExecutor>())
1361 }
1362
1363 async fn get_note_updates(
1366 &self,
1367 submission_height: BlockNumber,
1368 tx_result: &TransactionResult,
1369 ) -> Result<NoteUpdateTracker, TransactionStoreUpdateError> {
1370 let executed_tx = tx_result.executed_transaction();
1371 let current_timestamp = self.store.get_current_timestamp();
1372 let current_block_num = self.store.get_sync_height().await?;
1373
1374 let new_output_notes = executed_tx
1386 .output_notes()
1387 .iter()
1388 .filter(|output_note| {
1389 output_note
1390 .recipient()
1391 .is_none_or(|recipient| recipient.script().root() != TxFeeNote::script_root())
1392 })
1393 .cloned()
1394 .filter_map(|output_note| {
1395 OutputNoteRecord::try_from_output_note(output_note, submission_height).ok()
1396 })
1397 .collect::<Vec<_>>();
1398
1399 let mut new_input_notes = vec![];
1401 let output_notes: Vec<Note> =
1402 notes_from_output(executed_tx.output_notes()).cloned().collect();
1403 let note_screener = self.note_screener().clone();
1404 let output_note_relevances = note_screener.get_batch_consumability(&output_notes).await?;
1405
1406 for note in output_notes {
1407 if note.script().root() == TxFeeNote::script_root() {
1412 continue;
1413 }
1414
1415 if output_note_relevances.contains_key(¬e.id()) {
1416 let metadata = *note.metadata();
1417 let tag = metadata.tag();
1418 let attachments = note.attachments().clone();
1419
1420 new_input_notes.push(InputNoteRecord::new(
1421 note.into(),
1422 attachments,
1423 current_timestamp,
1424 ExpectedNoteState {
1425 metadata: Some(metadata),
1426 after_block_num: submission_height,
1427 tag: Some(tag),
1428 }
1429 .into(),
1430 ));
1431 }
1432 }
1433
1434 new_input_notes.extend(tx_result.future_notes().iter().map(|(note_details, tag)| {
1436 InputNoteRecord::new(
1437 note_details.clone(),
1438 NoteAttachments::empty(),
1439 None,
1440 ExpectedNoteState {
1441 metadata: None,
1442 after_block_num: current_block_num,
1443 tag: Some(*tag),
1444 }
1445 .into(),
1446 )
1447 }));
1448
1449 let consumed_note_ids =
1454 executed_tx.tx_inputs().input_notes().iter().map(InputNote::id).collect();
1455
1456 let consumed_notes =
1457 self.store.get_input_notes(NoteFilter::List(consumed_note_ids)).await?;
1458
1459 let tracked_note_ids =
1460 consumed_notes.iter().filter_map(InputNoteRecord::id).collect::<BTreeSet<_>>();
1461
1462 for input_note in executed_tx.tx_inputs().input_notes() {
1463 if !tracked_note_ids.contains(&input_note.id()) {
1464 let mut input_note_record = InputNoteRecord::from(input_note.clone());
1465 input_note_record.consumed_locally(
1466 executed_tx.account_id(),
1467 executed_tx.id(),
1468 current_timestamp,
1469 )?;
1470 new_input_notes.push(input_note_record);
1471 }
1472 }
1473
1474 let mut updated_input_notes = vec![];
1475
1476 for mut input_note_record in consumed_notes {
1477 if input_note_record.consumed_locally(
1478 executed_tx.account_id(),
1479 executed_tx.id(),
1480 current_timestamp,
1481 )? {
1482 updated_input_notes.push(input_note_record);
1483 }
1484 }
1485
1486 Ok(NoteUpdateTracker::for_transaction_updates(
1487 new_input_notes,
1488 updated_input_notes,
1489 new_output_notes,
1490 ))
1491 }
1492}
1493
1494#[derive(Debug, thiserror::Error)]
1500pub enum TransactionStoreUpdateError {
1501 #[error("store error")]
1502 Store(#[from] StoreError),
1503 #[error("note screener error")]
1504 NoteScreener(#[from] NoteScreenerError),
1505 #[error("note record error")]
1506 NoteRecord(#[from] NoteRecordError),
1507}
1508
1509#[derive(Clone, Copy, Debug)]
1513enum TransactionExecutionMode {
1514 Standard,
1515 #[cfg(feature = "dap")]
1516 Dap,
1517}
1518
1519pub(crate) struct PreparedTransaction {
1521 pub(crate) notes: InputNotes<InputNote>,
1522 pub(crate) output_recipients: Vec<NoteRecipient>,
1523 pub(crate) future_notes: Vec<(NoteDetails, NoteTag)>,
1524 pub(crate) tx_args: TransactionArgs,
1525 pub(crate) foreign_account_inputs: Vec<AccountInputs>,
1526 pub(crate) block_numbers: BTreeSet<BlockNumber>,
1527 pub(crate) block_num: BlockNumber,
1528 pub(crate) ignore_invalid_notes: bool,
1529}
1530
1531impl PreparedTransaction {
1532 pub(crate) fn output_note_scripts(&self) -> impl Iterator<Item = NoteScript> + '_ {
1535 self.output_recipients.iter().map(|recipient| recipient.script().clone())
1536 }
1537}
1538
1539fn get_outgoing_assets(
1544 transaction_request: &TransactionRequest,
1545) -> (BTreeMap<AccountId, u64>, Vec<Asset>) {
1546 let mut own_notes_assets = match transaction_request.script_template() {
1547 Some(TransactionScriptTemplate::SendNotes(notes)) => notes
1548 .iter()
1549 .map(|note| (note.id(), note.assets().clone()))
1550 .collect::<BTreeMap<_, _>>(),
1551 _ => BTreeMap::default(),
1552 };
1553 let mut output_notes_assets = transaction_request
1554 .expected_output_own_notes()
1555 .into_iter()
1556 .map(|note| (note.id(), note.assets().clone()))
1557 .collect::<BTreeMap<_, _>>();
1558
1559 output_notes_assets.append(&mut own_notes_assets);
1561
1562 let outgoing_assets = output_notes_assets.values().flat_map(|note_assets| note_assets.iter());
1564
1565 request::collect_assets(outgoing_assets)
1566}
1567
1568fn attach_default_auth_args(
1574 transaction_request: &mut TransactionRequest,
1575 account_code_interface: &AccountCodeInterface,
1576 reference_header: &BlockHeader,
1577 protocol_config: &ProtocolConfig,
1578) -> Result<(), ClientError> {
1579 if transaction_request.has_auth_arg() {
1583 return Ok(());
1584 }
1585
1586 if let Some((auth_arg, preimage)) = AuthArgsLayout::of(account_code_interface)
1587 .default_auth_args(
1588 transaction_request.fee_conversion_salt(),
1589 reference_header,
1590 protocol_config,
1591 )?
1592 {
1593 transaction_request.commit_auth_args(auth_arg, preimage);
1594 }
1595
1596 Ok(())
1597}
1598
1599pub(crate) enum AuthArgsLayout {
1601 SingleSig,
1604 Multisig(String),
1607 Unsupported(String),
1610}
1611
1612impl AuthArgsLayout {
1613 pub(crate) fn of(account_code_interface: &AccountCodeInterface) -> Self {
1618 let procedures: Vec<_> = account_code_interface.procedures().iter().copied().collect();
1619
1620 AccountComponentInterface::from_procedures(&procedures)
1621 .iter()
1622 .find_map(|component| match component {
1623 AccountComponentInterface::AuthSingleSig => Some(Self::SingleSig),
1624 AccountComponentInterface::AuthMultisig
1625 | AccountComponentInterface::AuthMultisigSmart
1626 | AccountComponentInterface::AuthGuardedMultisig => {
1627 Some(Self::Multisig(component.name()))
1628 },
1629 AccountComponentInterface::AuthNoAuth
1630 | AccountComponentInterface::AuthNetworkAccount => {
1631 Some(Self::Unsupported(component.name()))
1632 },
1633 _ => None,
1634 })
1635 .unwrap_or_else(|| Self::Unsupported("unrecognized".into()))
1636 }
1637
1638 fn bound_block(&self, transaction_request: &TransactionRequest) -> Option<BlockNumber> {
1643 if !matches!(self, Self::Multisig(_)) || !transaction_request.has_auth_arg() {
1644 return None;
1645 }
1646
1647 let auth_arg = (*transaction_request.auth_arg())?;
1648 let preimage = transaction_request.advice_map().get(&auth_arg)?;
1649 let bound_block = u32::try_from(preimage.first()?.as_canonical_u64()).ok()?;
1650
1651 Some(BlockNumber::from(bound_block))
1652 }
1653
1654 pub(crate) fn default_auth_args(
1665 &self,
1666 declared_salt: Option<Word>,
1667 reference_header: &BlockHeader,
1668 protocol_config: &ProtocolConfig,
1669 ) -> Result<Option<(Word, Vec<Felt>)>, TransactionRequestError> {
1670 let fee_charged = reference_header.fee_parameters().verification_base_fee() != 0;
1671 let conversion_info =
1672 FeeConversionInfo::one_to_one(protocol_config.fee_asset_id().faucet_id());
1673
1674 match (self, declared_salt) {
1675 (Self::SingleSig, None) if !fee_charged => Ok(None),
1676 (Self::SingleSig, salt) => Ok(Some(commit_fee_conversion_info(
1677 conversion_info,
1678 salt.unwrap_or(NATIVE_FEE_CONVERSION_SALT),
1679 ))),
1680 (Self::Multisig(_), Some(salt)) => {
1681 let auth_args = MultisigAuthArgs::new(reference_header.block_num(), salt)
1682 .with_conversion_info(conversion_info);
1683 Ok(Some((auth_args.to_commitment(), auth_args.to_elements())))
1684 },
1685 (Self::Multisig(component), None) => {
1686 Err(TransactionRequestError::FeeConversionInfoRequired(component.clone()))
1687 },
1688 (Self::Unsupported(component), Some(_)) => {
1689 Err(TransactionRequestError::FeeConversionInfoUnsupported(component.clone()))
1690 },
1691 (Self::Unsupported(_), None) => Ok(None),
1692 }
1693 }
1694}
1695
1696fn validate_output_note_senders(
1704 transaction_request: &TransactionRequest,
1705 account_id: AccountId,
1706) -> Result<(), ClientError> {
1707 for note in transaction_request.expected_output_own_notes() {
1708 let sender = note.metadata().sender();
1709 if sender != account_id {
1710 return Err(ClientError::TransactionRequestError(
1711 TransactionRequestError::OutputNoteSenderMismatch {
1712 expected: account_id,
1713 actual: sender,
1714 },
1715 ));
1716 }
1717 }
1718
1719 Ok(())
1720}
1721
1722fn validate_basic_account_request(
1725 transaction_request: &TransactionRequest,
1726 vault_assets: &[Asset],
1727) -> Result<(), ClientError> {
1728 let (fungible_balance_map, non_fungible_set) = get_outgoing_assets(transaction_request);
1729 let (incoming_fungible_balance_map, incoming_non_fungible_balance_set) =
1730 transaction_request.incoming_assets();
1731
1732 let mut available_fungible: BTreeMap<AccountId, u64> = BTreeMap::new();
1735 for asset in vault_assets {
1736 if let Some(fungible) = asset.as_fungible() {
1737 let balance = available_fungible.entry(fungible.faucet_id()).or_default();
1738 *balance = balance.saturating_add(fungible.amount().as_u64());
1739 }
1740 }
1741
1742 for (faucet_id, amount) in fungible_balance_map {
1745 let account_asset_amount = available_fungible.get(&faucet_id).copied().unwrap_or(0);
1746 let incoming_balance = incoming_fungible_balance_map.get(&faucet_id).unwrap_or(&0);
1747 if account_asset_amount + incoming_balance < amount {
1748 return Err(ClientError::AssetError(AssetError::FungibleAssetAmountNotSufficient {
1749 minuend: account_asset_amount,
1750 subtrahend: amount,
1751 }));
1752 }
1753 }
1754
1755 for non_fungible in &non_fungible_set {
1758 let held = vault_assets.iter().any(|asset| asset == non_fungible);
1759 if !held && !incoming_non_fungible_balance_set.contains(non_fungible) {
1760 return Err(ClientError::TransactionRequestError(
1761 TransactionRequestError::MissingNonFungibleAsset(non_fungible.faucet_id()),
1762 ));
1763 }
1764 }
1765
1766 Ok(())
1767}
1768
1769async fn local_account_inputs(
1779 store: &Arc<dyn Store>,
1780 rpc_api: &Arc<dyn NodeRpcClient>,
1781 account_id: AccountId,
1782 account_state_at: AccountStateAt,
1783) -> Result<Option<AccountInputs>, ClientError> {
1784 let AccountStateAt::Block(block_num) = account_state_at else {
1786 return Ok(None);
1787 };
1788 if block_num != store.get_sync_height().await? {
1789 return Ok(None);
1790 }
1791 let Some(record) = store.get_minimal_partial_account(account_id).await? else {
1792 return Ok(None);
1793 };
1794 let account: PartialAccount = record.try_into()?;
1797
1798 let witness = if let Some(witness) = store.get_account_witness(account_id).await? {
1799 witness
1800 } else {
1801 let (_, account_proof) = rpc_api
1803 .get_account(
1804 GetAccountRequest::new(account_id)
1805 .at(account_state_at)
1806 .with_known_code(Some(account.code().clone())),
1807 )
1808 .await?;
1809 account_proof.into_parts().0
1810 };
1811
1812 if account.to_commitment() == witness.state_commitment() {
1813 return Ok(Some(AccountInputs::new(account, witness)));
1814 }
1815
1816 Ok(None)
1817}
1818
1819pub(crate) async fn fetch_public_account_inputs(
1829 store: &Arc<dyn Store>,
1830 rpc_api: &Arc<dyn NodeRpcClient>,
1831 account_id: AccountId,
1832 fetch: AccountFetch,
1833 account_state_at: AccountStateAt,
1834) -> Result<AccountInputs, ClientError> {
1835 if let Some(inputs) =
1837 Box::pin(local_account_inputs(store, rpc_api, account_id, account_state_at)).await?
1838 {
1839 return Ok(inputs);
1840 }
1841
1842 let known_code: Option<AccountCode> =
1843 store.get_foreign_account_code(vec![account_id]).await?.into_values().next();
1844
1845 let (_block_num, account_proof) = rpc_api
1846 .get_account(
1847 GetAccountRequest::new(account_id)
1848 .with_fetch(fetch)
1849 .at(account_state_at)
1850 .with_known_code(known_code),
1851 )
1852 .await?;
1853
1854 let account_inputs = account_proof.into_account_inputs()?;
1855
1856 let _ = store
1857 .upsert_foreign_account_code(account_id, account_inputs.code().clone())
1858 .await
1859 .inspect_err(|err| {
1860 tracing::warn!(
1861 %account_id,
1862 %err,
1863 "Failed to persist foreign account code to store"
1864 );
1865 });
1866
1867 Ok(account_inputs)
1868}
1869
1870fn promote_indeterminate_submission(
1873 err: RpcError,
1874 transaction: ProvenTransaction,
1875 transaction_inputs: TransactionInputs,
1876) -> ClientError {
1877 if !err.is_indeterminate_submission() {
1878 return ClientError::RpcError(err);
1879 }
1880
1881 ClientError::SubmissionOutcomeUnknown {
1882 transaction: Box::new(transaction),
1883 transaction_inputs: Box::new(transaction_inputs),
1884 source: err,
1885 }
1886}
1887
1888pub fn notes_from_output(output_notes: &RawOutputNotes) -> impl Iterator<Item = &Note> {
1893 output_notes.iter().filter_map(|n| match n {
1894 RawOutputNote::Full(n) => Some(n),
1895 RawOutputNote::Partial(_) => None,
1896 })
1897}
1898
1899pub(crate) fn validate_executed_transaction(
1902 executed_transaction: &ExecutedTransaction,
1903 expected_output_recipients: &[NoteRecipient],
1904) -> Result<(), ClientError> {
1905 let tx_output_recipient_digests = executed_transaction
1906 .output_notes()
1907 .iter()
1908 .filter_map(|n| n.recipient().map(NoteRecipient::digest))
1909 .collect::<Vec<_>>();
1910
1911 let missing_recipient_digest: Vec<Word> = expected_output_recipients
1912 .iter()
1913 .filter_map(|recipient| {
1914 (!tx_output_recipient_digests.contains(&recipient.digest()))
1915 .then_some(recipient.digest())
1916 })
1917 .collect();
1918
1919 if !missing_recipient_digest.is_empty() {
1920 return Err(ClientError::MissingOutputRecipients(missing_recipient_digest));
1921 }
1922
1923 Ok(())
1924}
1925
1926fn ensure_account_allowed(
1931 account_id: AccountId,
1932 is_allowed: Result<bool, ClientError>,
1933) -> Result<(), ClientError> {
1934 match is_allowed {
1935 Ok(true) => Ok(()),
1936 Ok(false) => Err(ClientError::AccountNotAllowlisted(account_id)),
1937 Err(err) => {
1938 info!(
1939 "could not check whether account {account_id} is on the network allowlist, \
1940 submitting anyway and letting the node decide: {err}"
1941 );
1942 Ok(())
1943 },
1944 }
1945}
1946
1947#[cfg(test)]
1951mod tests {
1952 use alloc::vec;
1953
1954 use miden_protocol::Word;
1955 use miden_protocol::account::auth::AuthSecretKey;
1956 use miden_protocol::account::{
1957 Account,
1958 AccountBuilder,
1959 AccountComponent,
1960 AccountComponentMetadata,
1961 AccountId,
1962 AccountType,
1963 };
1964 use miden_protocol::asset::{AssetId, FungibleAsset};
1965 use miden_protocol::block::{BlockHeader, BlockNumber, FeeParameters};
1966 use miden_protocol::crypto::SequentialCommit;
1967 use miden_protocol::note::{Note, NoteType};
1968 use miden_protocol::protocol_config::ProtocolConfig;
1969 use miden_protocol::testing::account_id::{
1970 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
1971 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
1972 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
1973 ACCOUNT_ID_SENDER,
1974 };
1975 use miden_standards::account::AccountBuilderSchemaCommitmentExt;
1976 use miden_standards::account::auth::{
1977 Approver,
1978 ApproverSet,
1979 AuthGuardedMultisig,
1980 AuthGuardedMultisigConfig,
1981 AuthMultisig,
1982 AuthMultisigConfig,
1983 AuthMultisigSmart,
1984 AuthMultisigSmartConfig,
1985 AuthSingleSig,
1986 FeeConversionInfo,
1987 GuardianConfig,
1988 MultisigAuthArgs,
1989 NoAuth,
1990 commit_fee_conversion_info,
1991 };
1992 use miden_standards::account::wallets::BasicWallet;
1993 use miden_standards::note::P2idNote;
1994 use rand::SeedableRng;
1995 use rand_chacha::ChaCha20Rng;
1996
1997 use super::{
1998 AccountComponentInterface,
1999 NATIVE_FEE_CONVERSION_SALT,
2000 TransactionRequest,
2001 TransactionRequestBuilder,
2002 attach_default_auth_args,
2003 validate_output_note_senders,
2004 };
2005 use crate::ClientError;
2006 use crate::assembly::CodeBuilder;
2007 use crate::auth::AuthSchemeId;
2008 use crate::rng::draw_word;
2009 use crate::transaction::TransactionRequestError;
2010
2011 fn own_note_with_sender(sender: AccountId) -> Note {
2012 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
2013 let target_id =
2014 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
2015 let mut rng = ChaCha20Rng::seed_from_u64(0);
2016
2017 P2idNote::builder()
2018 .sender(sender)
2019 .target(target_id)
2020 .asset(FungibleAsset::new(faucet_id, 100).unwrap())
2021 .note_type(NoteType::Public)
2022 .serial_number(draw_word(&mut rng))
2023 .build()
2024 .expect("note creation failed")
2025 .into()
2026 }
2027
2028 #[test]
2029 fn output_note_with_foreign_sender_is_rejected() {
2030 let account_id =
2031 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
2032 let foreign_sender = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
2033 assert_ne!(account_id, foreign_sender);
2034
2035 let request = TransactionRequestBuilder::new()
2036 .own_output_notes(vec![own_note_with_sender(foreign_sender)])
2037 .build()
2038 .unwrap();
2039
2040 let err = validate_output_note_senders(&request, account_id).unwrap_err();
2041 match err {
2042 ClientError::TransactionRequestError(
2043 TransactionRequestError::OutputNoteSenderMismatch { expected, actual },
2044 ) => {
2045 assert_eq!(expected, account_id);
2046 assert_eq!(actual, foreign_sender);
2047 },
2048 other => panic!("expected OutputNoteSenderMismatch, got {other:?}"),
2049 }
2050 }
2051
2052 #[test]
2053 fn output_note_with_matching_sender_is_accepted() {
2054 let account_id =
2055 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
2056
2057 let request = TransactionRequestBuilder::new()
2058 .own_output_notes(vec![own_note_with_sender(account_id)])
2059 .build()
2060 .unwrap();
2061
2062 validate_output_note_senders(&request, account_id).unwrap();
2063 }
2064
2065 #[test]
2066 fn request_without_own_output_notes_is_accepted() {
2067 let account_id =
2068 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
2069 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
2070
2071 let request = TransactionRequestBuilder::new()
2073 .input_notes(vec![(own_note_with_sender(faucet_id), None)])
2074 .build()
2075 .unwrap();
2076
2077 validate_output_note_senders(&request, account_id).unwrap();
2078 }
2079
2080 fn account_with_auth(auth_component: impl Into<AccountComponent>) -> Account {
2082 AccountBuilder::new([7u8; 32])
2083 .account_type(AccountType::Public)
2084 .with_component(auth_component)
2085 .with_component(BasicWallet)
2086 .build_with_schema_commitment()
2087 .expect("account creation failed")
2088 }
2089
2090 fn fee_conversion_request() -> TransactionRequest {
2091 TransactionRequestBuilder::new()
2092 .fee_conversion_salt(Word::from([13u32, 14, 15, 16]))
2093 .build()
2094 .unwrap()
2095 }
2096
2097 #[test]
2098 fn fee_conversion_info_is_accepted_by_a_signature_authenticated_account() {
2099 let key = AuthSecretKey::new_falcon512_poseidon2();
2100 let auth = AuthSingleSig::new(Approver::new(
2101 key.public_key().to_commitment(),
2102 AuthSchemeId::Falcon512Poseidon2,
2103 ));
2104
2105 try_injected_auth_arg(fee_conversion_request(), &account_with_auth(auth), 500).unwrap();
2106 }
2107
2108 #[test]
2109 fn fee_conversion_info_is_rejected_by_an_account_that_cannot_read_it() {
2110 let account = account_with_auth(NoAuth);
2111
2112 let err = try_injected_auth_arg(fee_conversion_request(), &account, 500)
2113 .expect_err("NoAuth does not read the auth args");
2114 match err {
2115 ClientError::TransactionRequestError(
2116 TransactionRequestError::FeeConversionInfoUnsupported(auth_component),
2117 ) => assert_eq!(auth_component, AccountComponentInterface::AuthNoAuth.name()),
2118 other => panic!("expected FeeConversionInfoUnsupported, got {other:?}"),
2119 }
2120 }
2121
2122 #[test]
2123 fn a_request_without_fee_conversion_info_skips_the_auth_component_check() {
2124 try_injected_auth_arg(
2126 TransactionRequestBuilder::new().build().unwrap(),
2127 &account_with_auth(NoAuth),
2128 500,
2129 )
2130 .unwrap();
2131 }
2132
2133 const NATIVE_FEE_FAUCET: u128 = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET;
2139
2140 fn test_protocol_config() -> ProtocolConfig {
2141 ProtocolConfig::current(AssetId::new_fungible(NATIVE_FEE_FAUCET.try_into().unwrap()))
2142 .unwrap()
2143 }
2144
2145 fn header_with_base_fee(verification_base_fee: u32) -> BlockHeader {
2147 let fee_parameters = FeeParameters::new(verification_base_fee);
2148 let (_, validator_keys) = miden_protocol::block::ValidatorConfig::random_with_signers(1);
2149
2150 BlockHeader::new(
2151 Word::empty(),
2152 BlockNumber::from(1u32),
2153 Word::empty(),
2154 Word::empty(),
2155 Word::empty(),
2156 Word::empty(),
2157 Word::empty(),
2158 validator_keys,
2159 fee_parameters,
2160 test_protocol_config().to_commitment(),
2161 None,
2162 0,
2163 )
2164 }
2165
2166 fn injected_auth_arg(
2169 mut request: TransactionRequest,
2170 account: &Account,
2171 verification_base_fee: u32,
2172 ) -> Option<Word> {
2173 let _ = attach_default_auth_args(
2174 &mut request,
2175 &account.code_interface(),
2176 &header_with_base_fee(verification_base_fee),
2177 &test_protocol_config(),
2178 );
2179 *request.auth_arg()
2180 }
2181
2182 fn try_injected_auth_arg(
2184 mut request: TransactionRequest,
2185 account: &Account,
2186 verification_base_fee: u32,
2187 ) -> Result<Option<Word>, ClientError> {
2188 attach_default_auth_args(
2189 &mut request,
2190 &account.code_interface(),
2191 &header_with_base_fee(verification_base_fee),
2192 &test_protocol_config(),
2193 )?;
2194 Ok(*request.auth_arg())
2195 }
2196
2197 fn singlesig_account() -> Account {
2198 let key = AuthSecretKey::new_falcon512_poseidon2();
2199 account_with_auth(AuthSingleSig::new(Approver::new(
2200 key.public_key().to_commitment(),
2201 AuthSchemeId::Falcon512Poseidon2,
2202 )))
2203 }
2204
2205 fn guarded_multisig_account() -> Account {
2206 let approvers = ApproverSet::new(
2207 vec![Approver::new(
2208 AuthSecretKey::new_falcon512_poseidon2().public_key().to_commitment(),
2209 AuthSchemeId::Falcon512Poseidon2,
2210 )],
2211 1,
2212 )
2213 .unwrap();
2214
2215 let guardian = GuardianConfig::new(Approver::new(
2216 AuthSecretKey::new_falcon512_poseidon2().public_key().to_commitment(),
2217 AuthSchemeId::Falcon512Poseidon2,
2218 ));
2219
2220 account_with_auth(
2221 AuthGuardedMultisig::new(AuthGuardedMultisigConfig::new(approvers, guardian).unwrap())
2222 .unwrap(),
2223 )
2224 }
2225
2226 #[test]
2227 fn native_fee_conversion_info_is_attached_on_a_fee_charging_chain() {
2228 let auth_arg = injected_auth_arg(
2229 TransactionRequestBuilder::new().build().unwrap(),
2230 &singlesig_account(),
2231 500,
2232 )
2233 .expect("a fee-charging chain should get conversion info attached");
2234
2235 let (expected, _) = commit_fee_conversion_info(
2236 FeeConversionInfo::one_to_one(AccountId::try_from(NATIVE_FEE_FAUCET).unwrap()),
2237 NATIVE_FEE_CONVERSION_SALT,
2238 );
2239 assert_eq!(auth_arg, expected, "the fee should be paid in the native asset at rate 1/1");
2240 }
2241
2242 #[test]
2243 #[allow(deprecated)]
2244 fn an_explicit_auth_arg_is_not_overwritten() {
2245 let auth_arg = Word::from([21u32, 22, 23, 24]);
2246 let request = TransactionRequestBuilder::new().auth_arg(auth_arg).build().unwrap();
2247
2248 assert_eq!(
2249 injected_auth_arg(request, &singlesig_account(), 500),
2250 Some(auth_arg),
2251 "a request that declares its own auth arg keeps it"
2252 );
2253 }
2254
2255 fn native_commitment(salt: Word) -> Word {
2257 let (auth_arg, _) = commit_fee_conversion_info(
2258 FeeConversionInfo::one_to_one(AccountId::try_from(NATIVE_FEE_FAUCET).unwrap()),
2259 salt,
2260 );
2261 auth_arg
2262 }
2263
2264 fn native_multisig_commitment(salt: Word) -> Word {
2265 MultisigAuthArgs::new(BlockNumber::from(1u32), salt)
2266 .with_conversion_info(FeeConversionInfo::one_to_one(
2267 AccountId::try_from(NATIVE_FEE_FAUCET).unwrap(),
2268 ))
2269 .to_commitment()
2270 }
2271
2272 #[test]
2273 fn a_declared_salt_is_used_for_the_native_commitment() {
2274 let salt = Word::from([17u32, 18, 19, 20]);
2275 let request = TransactionRequestBuilder::new().fee_conversion_salt(salt).build().unwrap();
2276
2277 let auth_arg = injected_auth_arg(request, &singlesig_account(), 500)
2278 .expect("a declared salt should still get native conversion info attached");
2279
2280 assert_eq!(auth_arg, native_commitment(salt));
2281 }
2282
2283 fn multisig_account() -> Account {
2284 let approvers = ApproverSet::new(
2285 vec![Approver::new(
2286 AuthSecretKey::new_falcon512_poseidon2().public_key().to_commitment(),
2287 AuthSchemeId::Falcon512Poseidon2,
2288 )],
2289 1,
2290 )
2291 .unwrap();
2292
2293 account_with_auth(AuthMultisig::new(AuthMultisigConfig::new(approvers)).unwrap())
2294 }
2295
2296 #[test]
2297 fn nothing_is_attached_where_it_is_not_needed_or_not_readable() {
2298 for (case, account, base_fee) in [
2299 ("a zero base fee charges nothing", singlesig_account(), 0),
2300 ("NoAuth never reads the auth args", account_with_auth(NoAuth), 500),
2301 ("a multisig salt is its own replay guard", multisig_account(), 500),
2302 ] {
2303 assert_eq!(
2304 injected_auth_arg(
2305 TransactionRequestBuilder::new().build().unwrap(),
2306 &account,
2307 base_fee
2308 ),
2309 None,
2310 "{case}"
2311 );
2312 }
2313 }
2314
2315 #[test]
2322 fn fee_conversion_info_is_accepted_by_a_guarded_multisig_account() {
2323 try_injected_auth_arg(fee_conversion_request(), &guarded_multisig_account(), 500)
2324 .expect("a guarded multisig reads the auth args as conversion info");
2325 }
2326
2327 #[test]
2329 fn a_guarded_multisig_account_must_declare_its_own_fee_conversion_info() {
2330 let err = try_injected_auth_arg(
2331 TransactionRequestBuilder::new().build().unwrap(),
2332 &guarded_multisig_account(),
2333 500,
2334 )
2335 .expect_err("a guarded multisig account cannot inherit the fixed native salt");
2336 match err {
2337 ClientError::TransactionRequestError(
2338 TransactionRequestError::FeeConversionInfoRequired(auth_component),
2339 ) => {
2340 assert_eq!(auth_component, AccountComponentInterface::AuthGuardedMultisig.name());
2341 },
2342 other => panic!("expected FeeConversionInfoRequired, got {other:?}"),
2343 }
2344
2345 let salt = Word::from([13u32, 14, 15, 16]);
2346 assert_eq!(
2347 try_injected_auth_arg(fee_conversion_request(), &guarded_multisig_account(), 500)
2348 .expect("a declared salt is accepted"),
2349 Some(native_multisig_commitment(salt)),
2350 "a guarded multisig account that declares a salt commits the native conversion info"
2351 );
2352 }
2353
2354 fn smart_multisig_account() -> Account {
2358 let approvers = ApproverSet::new(
2359 vec![Approver::new(
2360 AuthSecretKey::new_falcon512_poseidon2().public_key().to_commitment(),
2361 AuthSchemeId::Falcon512Poseidon2,
2362 )],
2363 1,
2364 )
2365 .unwrap();
2366
2367 account_with_auth(AuthMultisigSmart::new(AuthMultisigSmartConfig::new(approvers)).unwrap())
2368 }
2369
2370 #[test]
2372 fn a_multisig_account_must_declare_a_salt_on_a_chain_without_fees() {
2373 let err = try_injected_auth_arg(
2374 TransactionRequestBuilder::new().build().unwrap(),
2375 &guarded_multisig_account(),
2376 0,
2377 )
2378 .expect_err("a multisig account always reads auth args");
2379 assert!(matches!(
2380 err,
2381 ClientError::TransactionRequestError(
2382 TransactionRequestError::FeeConversionInfoRequired(_)
2383 )
2384 ));
2385 }
2386
2387 #[test]
2389 fn fee_conversion_info_is_accepted_by_a_smart_multisig_account() {
2390 try_injected_auth_arg(fee_conversion_request(), &smart_multisig_account(), 500)
2391 .expect("a smart multisig reads the auth args as conversion info");
2392 }
2393
2394 #[test]
2396 fn a_smart_multisig_account_must_declare_its_own_fee_conversion_info() {
2397 let err = try_injected_auth_arg(
2398 TransactionRequestBuilder::new().build().unwrap(),
2399 &smart_multisig_account(),
2400 500,
2401 )
2402 .expect_err("a smart multisig account cannot inherit the fixed native salt");
2403 match err {
2404 ClientError::TransactionRequestError(
2405 TransactionRequestError::FeeConversionInfoRequired(auth_component),
2406 ) => {
2407 assert_eq!(auth_component, AccountComponentInterface::AuthMultisigSmart.name());
2408 },
2409 other => panic!("expected FeeConversionInfoRequired, got {other:?}"),
2410 }
2411
2412 let salt = Word::from([13u32, 14, 15, 16]);
2413 assert_eq!(
2414 try_injected_auth_arg(fee_conversion_request(), &smart_multisig_account(), 500)
2415 .expect("a declared salt is accepted"),
2416 Some(native_multisig_commitment(salt)),
2417 "a smart multisig account that declares a salt commits the native conversion info"
2418 );
2419 }
2420
2421 #[test]
2424 fn an_unrecognized_auth_component_is_rejected_rather_than_panicking() {
2425 const CUSTOM_AUTH: &str = "
2426 use miden::protocol::native_account
2427
2428 @auth_script
2429 pub proc auth_custom
2430 exec.native_account::incr_nonce
2431 drop
2432 end
2433 ";
2434
2435 let code = CodeBuilder::default()
2436 .compile_component_code("miden::testing::custom_auth", CUSTOM_AUTH)
2437 .expect("custom auth component code should compile");
2438 let auth = AccountComponent::new(
2439 code,
2440 vec![],
2441 AccountComponentMetadata::new("miden::testing::custom_auth"),
2442 )
2443 .expect("custom auth component");
2444
2445 let account = account_with_auth(auth);
2446
2447 let err = try_injected_auth_arg(fee_conversion_request(), &account, 500)
2448 .expect_err("an account with no recognized auth component cannot read conversion info");
2449 assert!(matches!(
2450 err,
2451 ClientError::TransactionRequestError(
2452 TransactionRequestError::FeeConversionInfoUnsupported(_)
2453 )
2454 ));
2455
2456 assert_eq!(
2457 try_injected_auth_arg(TransactionRequestBuilder::new().build().unwrap(), &account, 500)
2458 .expect("a request declaring nothing is left alone"),
2459 None,
2460 "an auth component nothing can reason about gets nothing attached"
2461 );
2462 }
2463}