1use alloc::boxed::Box;
66use alloc::collections::{BTreeMap, BTreeSet};
67use alloc::sync::Arc;
68use alloc::vec::Vec;
69
70use miden_protocol::account::{Account, AccountCode, AccountId};
71use miden_protocol::asset::NonFungibleAsset;
72use miden_protocol::block::BlockNumber;
73use miden_protocol::errors::AssetError;
74use miden_protocol::note::{Note, NoteDetails, NoteId, NoteRecipient, NoteScript, NoteTag};
75use miden_protocol::transaction::AccountInputs;
76use miden_protocol::{EMPTY_WORD, Felt, Word};
77use miden_standards::account::interface::AccountInterfaceExt;
78use miden_tx::{DataStore, NoteConsumptionChecker, TransactionExecutor};
79use tracing::info;
80
81use super::Client;
82use crate::ClientError;
83use crate::note::NoteUpdateTracker;
84use crate::rpc::domain::account::AccountStorageRequirements;
85use crate::rpc::{AccountStateAt, GrpcError, NodeRpcClient, RpcError};
86use crate::store::data_store::ClientDataStore;
87use crate::store::input_note_states::ExpectedNoteState;
88use crate::store::{
89 InputNoteRecord,
90 InputNoteState,
91 NoteFilter,
92 OutputNoteRecord,
93 Store,
94 TransactionFilter,
95};
96use crate::sync::NoteTagRecord;
97
98mod prover;
99pub use prover::TransactionProver;
100
101mod record;
102pub use record::{
103 DiscardCause,
104 TransactionDetails,
105 TransactionRecord,
106 TransactionStatus,
107 TransactionStatusVariant,
108};
109
110mod store_update;
111pub use store_update::TransactionStoreUpdate;
112
113mod request;
114pub use request::{
115 ForeignAccount,
116 NoteArgs,
117 PaymentNoteDescription,
118 SwapTransactionData,
119 TransactionRequest,
120 TransactionRequestBuilder,
121 TransactionRequestError,
122 TransactionScriptTemplate,
123};
124
125mod result;
126pub use miden_protocol::transaction::{
129 ExecutedTransaction,
130 InputNote,
131 InputNotes,
132 OutputNote,
133 OutputNotes,
134 ProvenTransaction,
135 PublicOutputNote,
136 RawOutputNote,
137 RawOutputNotes,
138 TransactionArgs,
139 TransactionId,
140 TransactionInputs,
141 TransactionKernel,
142 TransactionScript,
143 TransactionSummary,
144};
145pub use miden_protocol::vm::{AdviceInputs, AdviceMap};
146pub use miden_standards::account::interface::{AccountComponentInterface, AccountInterface};
147pub use miden_tx::auth::TransactionAuthenticator;
148pub use miden_tx::{
149 DataStoreError,
150 LocalTransactionProver,
151 ProvingOptions,
152 TransactionExecutorError,
153 TransactionProverError,
154};
155pub use result::TransactionResult;
156
157impl<AUTH> Client<AUTH>
159where
160 AUTH: TransactionAuthenticator + Sync + 'static,
161{
162 pub async fn get_transactions(
167 &self,
168 filter: TransactionFilter,
169 ) -> Result<Vec<TransactionRecord>, ClientError> {
170 self.store.get_transactions(filter).await.map_err(Into::into)
171 }
172
173 pub async fn submit_new_transaction(
186 &mut self,
187 account_id: AccountId,
188 transaction_request: TransactionRequest,
189 ) -> Result<TransactionId, ClientError> {
190 let prover = self.tx_prover.clone();
191 self.submit_new_transaction_with_prover(account_id, transaction_request, prover)
192 .await
193 }
194
195 pub async fn submit_new_transaction_with_prover(
206 &mut self,
207 account_id: AccountId,
208 transaction_request: TransactionRequest,
209 tx_prover: Arc<dyn TransactionProver>,
210 ) -> Result<TransactionId, ClientError> {
211 if !transaction_request.expected_ntx_scripts().is_empty() {
214 Box::pin(self.ensure_ntx_scripts_registered(
215 account_id,
216 transaction_request.expected_ntx_scripts(),
217 tx_prover.clone(),
218 ))
219 .await?;
220 }
221
222 let tx_result = self.execute_transaction(account_id, transaction_request).await?;
223 let tx_id = tx_result.executed_transaction().id();
224
225 let proven_transaction = self.prove_transaction_with(&tx_result, tx_prover).await?;
226 let submission_height =
227 self.submit_proven_transaction(proven_transaction, &tx_result).await?;
228
229 self.apply_transaction(&tx_result, submission_height).await?;
230
231 Ok(tx_id)
232 }
233
234 pub async fn execute_transaction(
248 &mut self,
249 account_id: AccountId,
250 transaction_request: TransactionRequest,
251 ) -> Result<TransactionResult, ClientError> {
252 self.validate_request(account_id, &transaction_request).await?;
254
255 let mut stored_note_records = self
257 .store
258 .get_input_notes(NoteFilter::List(transaction_request.input_note_ids().collect()))
259 .await?;
260
261 for note in &stored_note_records {
263 if note.is_consumed() {
264 return Err(ClientError::TransactionRequestError(
265 TransactionRequestError::InputNoteAlreadyConsumed(note.id()),
266 ));
267 }
268 }
269
270 stored_note_records.retain(InputNoteRecord::is_authenticated);
272
273 let authenticated_note_ids =
274 stored_note_records.iter().map(InputNoteRecord::id).collect::<Vec<_>>();
275
276 let unauthenticated_input_notes = transaction_request
281 .input_notes()
282 .iter()
283 .filter(|n| !authenticated_note_ids.contains(&n.id()))
284 .cloned()
285 .map(Into::into)
286 .collect::<Vec<_>>();
287
288 self.store.upsert_input_notes(&unauthenticated_input_notes).await?;
289
290 let mut notes = transaction_request.build_input_notes(stored_note_records)?;
291
292 let output_recipients =
293 transaction_request.expected_output_recipients().cloned().collect::<Vec<_>>();
294
295 let future_notes: Vec<(NoteDetails, NoteTag)> =
296 transaction_request.expected_future_notes().cloned().collect();
297
298 let tx_script = transaction_request
299 .build_transaction_script(&self.get_account_interface(account_id).await?)?;
300
301 let foreign_accounts = transaction_request.foreign_accounts().clone();
302
303 let (fpi_block_num, foreign_account_inputs) =
305 self.retrieve_foreign_account_inputs(foreign_accounts).await?;
306
307 let ignore_invalid_notes = transaction_request.ignore_invalid_input_notes();
308
309 let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
310 data_store.register_foreign_account_inputs(foreign_account_inputs.iter().cloned());
311 for fpi_account in &foreign_account_inputs {
312 data_store.mast_store().load_account_code(fpi_account.code());
313 }
314
315 let output_note_scripts: Vec<NoteScript> = transaction_request
317 .expected_output_recipients()
318 .map(|n| n.script().clone())
319 .collect();
320 self.store.upsert_note_scripts(&output_note_scripts).await?;
321
322 let block_num = if let Some(block_num) = fpi_block_num {
323 block_num
324 } else {
325 self.store.get_sync_height().await?
326 };
327
328 let account_record = self
331 .store
332 .get_account(account_id)
333 .await?
334 .ok_or(ClientError::AccountDataNotFound(account_id))?;
335 let account: Account = account_record.try_into()?;
336 data_store.mast_store().load_account_code(account.code());
337
338 let tx_args = transaction_request.into_transaction_args(tx_script);
340
341 if ignore_invalid_notes {
342 notes = self.get_valid_input_notes(account, notes, tx_args.clone()).await?;
344 }
345
346 let executed_transaction = self
348 .build_executor(&data_store)?
349 .execute_transaction(account_id, block_num, notes, tx_args)
350 .await?;
351
352 validate_executed_transaction(&executed_transaction, &output_recipients)?;
353 TransactionResult::new(executed_transaction, future_notes)
354 }
355
356 pub async fn prove_transaction(
358 &mut self,
359 tx_result: &TransactionResult,
360 ) -> Result<ProvenTransaction, ClientError> {
361 self.prove_transaction_with(tx_result, self.tx_prover.clone()).await
362 }
363
364 pub async fn prove_transaction_with(
366 &mut self,
367 tx_result: &TransactionResult,
368 tx_prover: Arc<dyn TransactionProver>,
369 ) -> Result<ProvenTransaction, ClientError> {
370 info!("Proving transaction...");
371
372 let proven_transaction =
373 tx_prover.prove(tx_result.executed_transaction().clone().into()).await?;
374
375 info!("Transaction proven.");
376
377 Ok(proven_transaction)
378 }
379
380 pub async fn submit_proven_transaction(
383 &mut self,
384 proven_transaction: ProvenTransaction,
385 transaction_inputs: impl Into<TransactionInputs>,
386 ) -> Result<BlockNumber, ClientError> {
387 info!("Submitting transaction to the network...");
388 let block_num = self
389 .rpc_api
390 .submit_proven_transaction(proven_transaction, transaction_inputs.into())
391 .await?;
392 info!("Transaction submitted.");
393
394 Ok(block_num)
395 }
396
397 pub async fn get_transaction_store_update(
400 &self,
401 tx_result: &TransactionResult,
402 submission_height: BlockNumber,
403 ) -> Result<TransactionStoreUpdate, ClientError> {
404 let note_updates = self.get_note_updates(submission_height, tx_result).await?;
405
406 let mut new_tags: Vec<NoteTagRecord> = note_updates
407 .updated_input_notes()
408 .filter_map(|note| {
409 let note = note.inner();
410
411 if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) =
412 note.state()
413 {
414 Some(NoteTagRecord::with_note_source(*tag, note.id()))
415 } else {
416 None
417 }
418 })
419 .collect();
420
421 new_tags.extend(note_updates.updated_output_notes().map(|note| {
423 let note = note.inner();
424 NoteTagRecord::with_note_source(note.metadata().tag(), note.id())
425 }));
426
427 Ok(TransactionStoreUpdate::new(
428 tx_result.executed_transaction().clone(),
429 submission_height,
430 note_updates,
431 tx_result.future_notes().to_vec(),
432 new_tags,
433 ))
434 }
435
436 pub async fn apply_transaction(
439 &self,
440 tx_result: &TransactionResult,
441 submission_height: BlockNumber,
442 ) -> Result<(), ClientError> {
443 let tx_update = self.get_transaction_store_update(tx_result, submission_height).await?;
444
445 self.apply_transaction_update(tx_update).await
446 }
447
448 pub async fn apply_transaction_update(
449 &self,
450 tx_update: TransactionStoreUpdate,
451 ) -> Result<(), ClientError> {
452 info!("Applying transaction to the local store...");
455
456 let executed_transaction = tx_update.executed_transaction();
457 let account_id = executed_transaction.account_id();
458
459 if self.account_reader(account_id).status().await?.is_locked() {
460 return Err(ClientError::AccountLocked(account_id));
461 }
462
463 self.store.apply_transaction(tx_update).await?;
464 info!("Transaction stored.");
465 Ok(())
466 }
467
468 pub async fn execute_program(
473 &mut self,
474 account_id: AccountId,
475 tx_script: TransactionScript,
476 advice_inputs: AdviceInputs,
477 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
478 ) -> Result<[Felt; 16], ClientError> {
479 let (fpi_block_number, foreign_account_inputs) =
480 self.retrieve_foreign_account_inputs(foreign_accounts).await?;
481
482 let block_ref = if let Some(block_number) = fpi_block_number {
483 block_number
484 } else {
485 self.get_sync_height().await?
486 };
487
488 let account_record = self
489 .store
490 .get_account(account_id)
491 .await?
492 .ok_or(ClientError::AccountDataNotFound(account_id))?;
493
494 let account: Account = account_record.try_into()?;
495
496 let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
497
498 data_store.register_foreign_account_inputs(foreign_account_inputs.iter().cloned());
499
500 data_store.mast_store().load_account_code(account.code());
502
503 for fpi_account in &foreign_account_inputs {
504 data_store.mast_store().load_account_code(fpi_account.code());
505 }
506
507 Ok(self
508 .build_executor(&data_store)?
509 .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
510 .await?)
511 }
512
513 async fn get_note_updates(
526 &self,
527 submission_height: BlockNumber,
528 tx_result: &TransactionResult,
529 ) -> Result<NoteUpdateTracker, ClientError> {
530 let executed_tx = tx_result.executed_transaction();
531 let current_timestamp = self.store.get_current_timestamp();
532 let current_block_num = self.store.get_sync_height().await?;
533
534 let new_output_notes = executed_tx
536 .output_notes()
537 .iter()
538 .cloned()
539 .filter_map(|output_note| {
540 OutputNoteRecord::try_from_output_note(output_note, submission_height).ok()
541 })
542 .collect::<Vec<_>>();
543
544 let mut new_input_notes = vec![];
546 let output_notes =
547 notes_from_output(executed_tx.output_notes()).cloned().collect::<Vec<_>>();
548 let note_screener = self.note_screener();
549 let output_note_relevances = note_screener.can_consume_batch(&output_notes).await?;
550
551 for note in output_notes {
552 if output_note_relevances.contains_key(¬e.id()) {
553 let metadata = note.metadata().clone();
554 let tag = metadata.tag();
555
556 new_input_notes.push(InputNoteRecord::new(
557 note.into(),
558 current_timestamp,
559 ExpectedNoteState {
560 metadata: Some(metadata),
561 after_block_num: submission_height,
562 tag: Some(tag),
563 }
564 .into(),
565 ));
566 }
567 }
568
569 new_input_notes.extend(tx_result.future_notes().iter().map(|(note_details, tag)| {
571 InputNoteRecord::new(
572 note_details.clone(),
573 None,
574 ExpectedNoteState {
575 metadata: None,
576 after_block_num: current_block_num,
577 tag: Some(*tag),
578 }
579 .into(),
580 )
581 }));
582
583 let consumed_note_ids =
585 executed_tx.tx_inputs().input_notes().iter().map(InputNote::id).collect();
586
587 let consumed_notes = self.get_input_notes(NoteFilter::List(consumed_note_ids)).await?;
588
589 let mut updated_input_notes = vec![];
590
591 for mut input_note_record in consumed_notes {
592 if input_note_record.consumed_locally(
593 executed_tx.account_id(),
594 executed_tx.id(),
595 self.store.get_current_timestamp(),
596 )? {
597 updated_input_notes.push(input_note_record);
598 }
599 }
600
601 Ok(NoteUpdateTracker::for_transaction_updates(
602 new_input_notes,
603 updated_input_notes,
604 new_output_notes,
605 ))
606 }
607
608 pub async fn validate_request(
615 &mut self,
616 account_id: AccountId,
617 transaction_request: &TransactionRequest,
618 ) -> Result<(), ClientError> {
619 if let Some(max_block_number_delta) = self.max_block_number_delta {
620 let current_chain_tip =
621 self.rpc_api.get_block_header_by_number(None, false).await?.0.block_num();
622
623 if current_chain_tip > self.store.get_sync_height().await? + max_block_number_delta {
624 return Err(ClientError::RecencyConditionError(
625 "The client is too far behind the chain tip to execute the transaction",
626 ));
627 }
628 }
629
630 let account = self.try_get_account(account_id).await?;
631 if account.is_faucet() {
632 Ok(())
634 } else {
635 validate_basic_account_request(transaction_request, &account)
636 }
637 }
638
639 pub async fn ensure_ntx_scripts_registered(
649 &mut self,
650 account_id: AccountId,
651 scripts: &[NoteScript],
652 tx_prover: Arc<dyn TransactionProver>,
653 ) -> Result<(), ClientError> {
654 let mut missing_scripts = Vec::new();
655
656 for script in scripts {
657 let script_root = script.root();
658
659 match self.rpc_api.get_note_script_by_root(script_root).await {
661 Ok(_) => {},
662 Err(RpcError::RequestError { error_kind: GrpcError::NotFound, .. }) => {
663 missing_scripts.push(script.clone());
664 },
665 Err(other) => {
666 return Err(ClientError::NtxScriptRegistrationFailed {
667 script_root,
668 source: other,
669 });
670 },
671 }
672 }
673
674 if missing_scripts.is_empty() {
675 return Ok(());
676 }
677
678 let registration_request = TransactionRequestBuilder::new().build_register_note_scripts(
679 account_id,
680 missing_scripts,
681 self.rng(),
682 )?;
683
684 let tx_result = self.execute_transaction(account_id, registration_request).await?;
685 let proven = self.prove_transaction_with(&tx_result, tx_prover).await?;
686 let submission_height = self.submit_proven_transaction(proven, &tx_result).await?;
687 self.apply_transaction(&tx_result, submission_height).await?;
688
689 Ok(())
690 }
691
692 async fn get_valid_input_notes(
695 &self,
696 account: Account,
697 mut input_notes: InputNotes<InputNote>,
698 tx_args: TransactionArgs,
699 ) -> Result<InputNotes<InputNote>, ClientError> {
700 loop {
701 let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
702
703 data_store.mast_store().load_account_code(account.code());
704 let execution = NoteConsumptionChecker::new(&self.build_executor(&data_store)?)
705 .check_notes_consumability(
706 account.id(),
707 self.store.get_sync_height().await?,
708 input_notes.iter().map(|n| n.clone().into_note()).collect(),
709 tx_args.clone(),
710 )
711 .await?;
712
713 if execution.failed.is_empty() {
714 break;
715 }
716
717 let failed_note_ids: BTreeSet<NoteId> =
718 execution.failed.iter().map(|n| n.note.id()).collect();
719 let filtered_input_notes = InputNotes::new(
720 input_notes
721 .into_iter()
722 .filter(|note| !failed_note_ids.contains(¬e.id()))
723 .collect(),
724 )
725 .expect("Created from a valid input notes list");
726
727 input_notes = filtered_input_notes;
728 }
729
730 Ok(input_notes)
731 }
732
733 pub(crate) async fn get_account_interface(
735 &self,
736 account_id: AccountId,
737 ) -> Result<AccountInterface, ClientError> {
738 let account = self.try_get_account(account_id).await?;
739 Ok(AccountInterface::from_account(&account))
740 }
741
742 async fn retrieve_foreign_account_inputs(
753 &mut self,
754 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
755 ) -> Result<(Option<BlockNumber>, Vec<AccountInputs>), ClientError> {
756 if foreign_accounts.is_empty() {
757 return Ok((None, Vec::new()));
758 }
759
760 let block_num = self.get_sync_height().await?;
761 let mut return_foreign_account_inputs = Vec::with_capacity(foreign_accounts.len());
762
763 for foreign_account in foreign_accounts.into_values() {
764 let foreign_account_inputs = match foreign_account {
765 ForeignAccount::Public(account_id, storage_requirements) => {
766 fetch_public_account_inputs(
767 &self.store,
768 &self.rpc_api,
769 account_id,
770 storage_requirements,
771 AccountStateAt::Block(block_num),
772 )
773 .await?
774 },
775 ForeignAccount::Private(partial_account) => {
776 let account_id = partial_account.id();
777 let (_, account_proof) = self
778 .rpc_api
779 .get_account_proof(
780 account_id,
781 AccountStorageRequirements::default(),
782 AccountStateAt::Block(block_num),
783 None,
784 None,
785 )
786 .await?;
787 let (witness, _) = account_proof.into_parts();
788 AccountInputs::new(partial_account, witness)
789 },
790 };
791
792 return_foreign_account_inputs.push(foreign_account_inputs);
793 }
794
795 Ok((Some(block_num), return_foreign_account_inputs))
796 }
797
798 pub(crate) fn build_executor<'store, 'auth, STORE: DataStore + Sync>(
801 &'auth self,
802 data_store: &'store STORE,
803 ) -> Result<TransactionExecutor<'store, 'auth, STORE, AUTH>, TransactionExecutorError> {
804 let mut executor = TransactionExecutor::new(data_store).with_options(self.exec_options)?;
805 if let Some(authenticator) = self.authenticator.as_deref() {
806 executor = executor.with_authenticator(authenticator);
807 }
808 executor = executor.with_source_manager(self.source_manager.clone());
809
810 Ok(executor)
811 }
812}
813
814fn get_outgoing_assets(
822 transaction_request: &TransactionRequest,
823) -> (BTreeMap<AccountId, u64>, Vec<NonFungibleAsset>) {
824 let mut own_notes_assets = match transaction_request.script_template() {
826 Some(TransactionScriptTemplate::SendNotes(notes)) => notes
827 .iter()
828 .map(|note| (note.id(), note.assets().clone()))
829 .collect::<BTreeMap<_, _>>(),
830 _ => BTreeMap::default(),
831 };
832 let mut output_notes_assets = transaction_request
834 .expected_output_own_notes()
835 .into_iter()
836 .map(|note| (note.id(), note.assets().clone()))
837 .collect::<BTreeMap<_, _>>();
838
839 output_notes_assets.append(&mut own_notes_assets);
841
842 let outgoing_assets = output_notes_assets.values().flat_map(|note_assets| note_assets.iter());
844
845 request::collect_assets(outgoing_assets)
846}
847
848fn validate_basic_account_request(
851 transaction_request: &TransactionRequest,
852 account: &Account,
853) -> Result<(), ClientError> {
854 let (fungible_balance_map, non_fungible_set) = get_outgoing_assets(transaction_request);
856
857 let (incoming_fungible_balance_map, incoming_non_fungible_balance_set) =
859 transaction_request.incoming_assets();
860
861 for (faucet_id, amount) in fungible_balance_map {
864 let account_asset_amount = account.vault().get_balance(faucet_id).unwrap_or(0);
865 let incoming_balance = incoming_fungible_balance_map.get(&faucet_id).unwrap_or(&0);
866 if account_asset_amount + incoming_balance < amount {
867 return Err(ClientError::AssetError(AssetError::FungibleAssetAmountNotSufficient {
868 minuend: account_asset_amount,
869 subtrahend: amount,
870 }));
871 }
872 }
873
874 for non_fungible in &non_fungible_set {
877 match account.vault().has_non_fungible_asset(*non_fungible) {
878 Ok(true) => (),
879 Ok(false) => {
880 if !incoming_non_fungible_balance_set.contains(non_fungible) {
882 return Err(ClientError::AssetError(
883 AssetError::NonFungibleFaucetIdTypeMismatch(non_fungible.faucet_id()),
884 ));
885 }
886 },
887 _ => {
888 return Err(ClientError::AssetError(AssetError::NonFungibleFaucetIdTypeMismatch(
889 non_fungible.faucet_id(),
890 )));
891 },
892 }
893 }
894
895 Ok(())
896}
897
898pub(crate) async fn fetch_public_account_inputs(
905 store: &Arc<dyn Store>,
906 rpc_api: &Arc<dyn NodeRpcClient>,
907 account_id: AccountId,
908 storage_requirements: AccountStorageRequirements,
909 account_state_at: AccountStateAt,
910) -> Result<AccountInputs, ClientError> {
911 let known_account_code: Option<AccountCode> =
912 store.get_foreign_account_code(vec![account_id]).await?.into_values().next();
913
914 let (_, account_proof) = rpc_api
915 .get_account_proof(
916 account_id,
917 storage_requirements.clone(),
918 account_state_at,
919 known_account_code,
920 Some(EMPTY_WORD),
921 )
922 .await?;
923
924 let account_inputs = request::account_proof_into_inputs(account_proof, &storage_requirements)?;
925
926 let _ = store
927 .upsert_foreign_account_code(account_id, account_inputs.code().clone())
928 .await
929 .inspect_err(|err| {
930 tracing::warn!(
931 %account_id,
932 %err,
933 "Failed to persist foreign account code to store"
934 );
935 });
936
937 Ok(account_inputs)
938}
939
940pub fn notes_from_output(output_notes: &RawOutputNotes) -> impl Iterator<Item = &Note> {
945 output_notes.iter().filter_map(|n| match n {
946 RawOutputNote::Full(n) => Some(n),
947 RawOutputNote::Partial(_) => None,
948 })
949}
950
951fn validate_executed_transaction(
954 executed_transaction: &ExecutedTransaction,
955 expected_output_recipients: &[NoteRecipient],
956) -> Result<(), ClientError> {
957 let tx_output_recipient_digests = executed_transaction
958 .output_notes()
959 .iter()
960 .filter_map(|n| n.recipient().map(NoteRecipient::digest))
961 .collect::<Vec<_>>();
962
963 let missing_recipient_digest: Vec<Word> = expected_output_recipients
964 .iter()
965 .filter_map(|recipient| {
966 (!tx_output_recipient_digests.contains(&recipient.digest()))
967 .then_some(recipient.digest())
968 })
969 .collect();
970
971 if !missing_recipient_digest.is_empty() {
972 return Err(ClientError::MissingOutputRecipients(missing_recipient_digest));
973 }
974
975 Ok(())
976}