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.build_transaction_script(
299 &self.get_account_interface(account_id).await?,
300 self.source_manager.clone(),
301 )?;
302
303 let foreign_accounts = transaction_request.foreign_accounts().clone();
304
305 let (fpi_block_num, foreign_account_inputs) =
307 self.retrieve_foreign_account_inputs(foreign_accounts).await?;
308
309 let ignore_invalid_notes = transaction_request.ignore_invalid_input_notes();
310
311 let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
312 data_store.register_foreign_account_inputs(foreign_account_inputs.iter().cloned());
313 for fpi_account in &foreign_account_inputs {
314 data_store.mast_store().load_account_code(fpi_account.code());
315 }
316
317 let output_note_scripts: Vec<NoteScript> = transaction_request
319 .expected_output_recipients()
320 .map(|n| n.script().clone())
321 .collect();
322 self.store.upsert_note_scripts(&output_note_scripts).await?;
323
324 let block_num = if let Some(block_num) = fpi_block_num {
325 block_num
326 } else {
327 self.store.get_sync_height().await?
328 };
329
330 let account_record = self
333 .store
334 .get_account(account_id)
335 .await?
336 .ok_or(ClientError::AccountDataNotFound(account_id))?;
337 let account: Account = account_record.try_into()?;
338 data_store.mast_store().load_account_code(account.code());
339
340 let tx_args = transaction_request.into_transaction_args(tx_script);
342
343 if ignore_invalid_notes {
344 notes = self.get_valid_input_notes(account, notes, tx_args.clone()).await?;
346 }
347
348 let executed_transaction = self
350 .build_executor(&data_store)?
351 .execute_transaction(account_id, block_num, notes, tx_args)
352 .await?;
353
354 validate_executed_transaction(&executed_transaction, &output_recipients)?;
355 TransactionResult::new(executed_transaction, future_notes)
356 }
357
358 pub async fn prove_transaction(
360 &mut self,
361 tx_result: &TransactionResult,
362 ) -> Result<ProvenTransaction, ClientError> {
363 self.prove_transaction_with(tx_result, self.tx_prover.clone()).await
364 }
365
366 pub async fn prove_transaction_with(
368 &mut self,
369 tx_result: &TransactionResult,
370 tx_prover: Arc<dyn TransactionProver>,
371 ) -> Result<ProvenTransaction, ClientError> {
372 info!("Proving transaction...");
373
374 let proven_transaction =
375 tx_prover.prove(tx_result.executed_transaction().clone().into()).await?;
376
377 info!("Transaction proven.");
378
379 Ok(proven_transaction)
380 }
381
382 pub async fn submit_proven_transaction(
385 &mut self,
386 proven_transaction: ProvenTransaction,
387 transaction_inputs: impl Into<TransactionInputs>,
388 ) -> Result<BlockNumber, ClientError> {
389 info!("Submitting transaction to the network...");
390 let block_num = self
391 .rpc_api
392 .submit_proven_transaction(proven_transaction, transaction_inputs.into())
393 .await?;
394 info!("Transaction submitted.");
395
396 Ok(block_num)
397 }
398
399 pub async fn get_transaction_store_update(
402 &self,
403 tx_result: &TransactionResult,
404 submission_height: BlockNumber,
405 ) -> Result<TransactionStoreUpdate, ClientError> {
406 let note_updates = self.get_note_updates(submission_height, tx_result).await?;
407
408 let mut new_tags: Vec<NoteTagRecord> = note_updates
409 .updated_input_notes()
410 .filter_map(|note| {
411 let note = note.inner();
412
413 if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) =
414 note.state()
415 {
416 Some(NoteTagRecord::with_note_source(*tag, note.id()))
417 } else {
418 None
419 }
420 })
421 .collect();
422
423 new_tags.extend(note_updates.updated_output_notes().map(|note| {
425 let note = note.inner();
426 NoteTagRecord::with_note_source(note.metadata().tag(), note.id())
427 }));
428
429 Ok(TransactionStoreUpdate::new(
430 tx_result.executed_transaction().clone(),
431 submission_height,
432 note_updates,
433 tx_result.future_notes().to_vec(),
434 new_tags,
435 ))
436 }
437
438 pub async fn apply_transaction(
441 &self,
442 tx_result: &TransactionResult,
443 submission_height: BlockNumber,
444 ) -> Result<(), ClientError> {
445 let tx_update = self.get_transaction_store_update(tx_result, submission_height).await?;
446
447 self.apply_transaction_update(tx_update).await
448 }
449
450 pub async fn apply_transaction_update(
451 &self,
452 tx_update: TransactionStoreUpdate,
453 ) -> Result<(), ClientError> {
454 info!("Applying transaction to the local store...");
457
458 let executed_transaction = tx_update.executed_transaction();
459 let account_id = executed_transaction.account_id();
460
461 if self.account_reader(account_id).status().await?.is_locked() {
462 return Err(ClientError::AccountLocked(account_id));
463 }
464
465 self.store.apply_transaction(tx_update).await?;
466 info!("Transaction stored.");
467 Ok(())
468 }
469
470 pub async fn execute_program(
475 &mut self,
476 account_id: AccountId,
477 tx_script: TransactionScript,
478 advice_inputs: AdviceInputs,
479 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
480 ) -> Result<[Felt; 16], ClientError> {
481 let (data_store, block_ref) =
482 self.prepare_program_execution(account_id, foreign_accounts).await?;
483
484 Ok(self
485 .build_executor(&data_store)?
486 .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
487 .await?)
488 }
489
490 #[cfg(feature = "dap")]
493 pub async fn execute_program_with_dap(
494 &mut self,
495 account_id: AccountId,
496 tx_script: TransactionScript,
497 advice_inputs: AdviceInputs,
498 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
499 ) -> Result<[Felt; 16], ClientError> {
500 let (data_store, block_ref) =
501 self.prepare_program_execution(account_id, foreign_accounts).await?;
502
503 Ok(self
504 .build_dap_executor(&data_store)?
505 .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
506 .await?)
507 }
508
509 async fn get_note_updates(
522 &self,
523 submission_height: BlockNumber,
524 tx_result: &TransactionResult,
525 ) -> Result<NoteUpdateTracker, ClientError> {
526 let executed_tx = tx_result.executed_transaction();
527 let current_timestamp = self.store.get_current_timestamp();
528 let current_block_num = self.store.get_sync_height().await?;
529
530 let new_output_notes = executed_tx
532 .output_notes()
533 .iter()
534 .cloned()
535 .filter_map(|output_note| {
536 OutputNoteRecord::try_from_output_note(output_note, submission_height).ok()
537 })
538 .collect::<Vec<_>>();
539
540 let mut new_input_notes = vec![];
542 let output_notes =
543 notes_from_output(executed_tx.output_notes()).cloned().collect::<Vec<_>>();
544 let note_screener = self.note_screener();
545 let output_note_relevances = note_screener.can_consume_batch(&output_notes).await?;
546
547 for note in output_notes {
548 if output_note_relevances.contains_key(¬e.id()) {
549 let metadata = note.metadata().clone();
550 let tag = metadata.tag();
551
552 new_input_notes.push(InputNoteRecord::new(
553 note.into(),
554 current_timestamp,
555 ExpectedNoteState {
556 metadata: Some(metadata),
557 after_block_num: submission_height,
558 tag: Some(tag),
559 }
560 .into(),
561 ));
562 }
563 }
564
565 new_input_notes.extend(tx_result.future_notes().iter().map(|(note_details, tag)| {
567 InputNoteRecord::new(
568 note_details.clone(),
569 None,
570 ExpectedNoteState {
571 metadata: None,
572 after_block_num: current_block_num,
573 tag: Some(*tag),
574 }
575 .into(),
576 )
577 }));
578
579 let consumed_note_ids =
581 executed_tx.tx_inputs().input_notes().iter().map(InputNote::id).collect();
582
583 let consumed_notes = self.get_input_notes(NoteFilter::List(consumed_note_ids)).await?;
584
585 let mut updated_input_notes = vec![];
586
587 for mut input_note_record in consumed_notes {
588 if input_note_record.consumed_locally(
589 executed_tx.account_id(),
590 executed_tx.id(),
591 self.store.get_current_timestamp(),
592 )? {
593 updated_input_notes.push(input_note_record);
594 }
595 }
596
597 Ok(NoteUpdateTracker::for_transaction_updates(
598 new_input_notes,
599 updated_input_notes,
600 new_output_notes,
601 ))
602 }
603
604 pub async fn validate_request(
611 &mut self,
612 account_id: AccountId,
613 transaction_request: &TransactionRequest,
614 ) -> Result<(), ClientError> {
615 if let Some(max_block_number_delta) = self.max_block_number_delta {
616 let current_chain_tip =
617 self.rpc_api.get_block_header_by_number(None, false).await?.0.block_num();
618
619 if current_chain_tip > self.store.get_sync_height().await? + max_block_number_delta {
620 return Err(ClientError::RecencyConditionError(
621 "The client is too far behind the chain tip to execute the transaction",
622 ));
623 }
624 }
625
626 let account = self.try_get_account(account_id).await?;
627 if account.is_faucet() {
628 Ok(())
630 } else {
631 validate_basic_account_request(transaction_request, &account)
632 }
633 }
634
635 pub async fn ensure_ntx_scripts_registered(
645 &mut self,
646 account_id: AccountId,
647 scripts: &[NoteScript],
648 tx_prover: Arc<dyn TransactionProver>,
649 ) -> Result<(), ClientError> {
650 let mut missing_scripts = Vec::new();
651
652 for script in scripts {
653 let script_root = script.root();
654
655 match self.rpc_api.get_note_script_by_root(script_root).await {
657 Ok(_) => {},
658 Err(RpcError::RequestError { error_kind: GrpcError::NotFound, .. }) => {
659 missing_scripts.push(script.clone());
660 },
661 Err(other) => {
662 return Err(ClientError::NtxScriptRegistrationFailed {
663 script_root,
664 source: other,
665 });
666 },
667 }
668 }
669
670 if missing_scripts.is_empty() {
671 return Ok(());
672 }
673
674 let registration_request = TransactionRequestBuilder::new().build_register_note_scripts(
675 account_id,
676 missing_scripts,
677 self.rng(),
678 )?;
679
680 let tx_result = self.execute_transaction(account_id, registration_request).await?;
681 let proven = self.prove_transaction_with(&tx_result, tx_prover).await?;
682 let submission_height = self.submit_proven_transaction(proven, &tx_result).await?;
683 self.apply_transaction(&tx_result, submission_height).await?;
684
685 Ok(())
686 }
687
688 async fn get_valid_input_notes(
691 &self,
692 account: Account,
693 mut input_notes: InputNotes<InputNote>,
694 tx_args: TransactionArgs,
695 ) -> Result<InputNotes<InputNote>, ClientError> {
696 loop {
697 let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
698
699 data_store.mast_store().load_account_code(account.code());
700 let execution = NoteConsumptionChecker::new(&self.build_executor(&data_store)?)
701 .check_notes_consumability(
702 account.id(),
703 self.store.get_sync_height().await?,
704 input_notes.iter().map(|n| n.clone().into_note()).collect(),
705 tx_args.clone(),
706 )
707 .await?;
708
709 if execution.failed.is_empty() {
710 break;
711 }
712
713 let failed_note_ids: BTreeSet<NoteId> =
714 execution.failed.iter().map(|n| n.note.id()).collect();
715 let filtered_input_notes = InputNotes::new(
716 input_notes
717 .into_iter()
718 .filter(|note| !failed_note_ids.contains(¬e.id()))
719 .collect(),
720 )
721 .expect("Created from a valid input notes list");
722
723 input_notes = filtered_input_notes;
724 }
725
726 Ok(input_notes)
727 }
728
729 pub(crate) async fn get_account_interface(
731 &self,
732 account_id: AccountId,
733 ) -> Result<AccountInterface, ClientError> {
734 let account = self.try_get_account(account_id).await?;
735 Ok(AccountInterface::from_account(&account))
736 }
737
738 async fn retrieve_foreign_account_inputs(
749 &mut self,
750 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
751 ) -> Result<(Option<BlockNumber>, Vec<AccountInputs>), ClientError> {
752 if foreign_accounts.is_empty() {
753 return Ok((None, Vec::new()));
754 }
755
756 let block_num = self.get_sync_height().await?;
757 let mut return_foreign_account_inputs = Vec::with_capacity(foreign_accounts.len());
758
759 for foreign_account in foreign_accounts.into_values() {
760 let foreign_account_inputs = match foreign_account {
761 ForeignAccount::Public(account_id, storage_requirements) => {
762 fetch_public_account_inputs(
763 &self.store,
764 &self.rpc_api,
765 account_id,
766 storage_requirements,
767 AccountStateAt::Block(block_num),
768 )
769 .await?
770 },
771 ForeignAccount::Private(partial_account) => {
772 let account_id = partial_account.id();
773 let (_, account_proof) = self
774 .rpc_api
775 .get_account_proof(
776 account_id,
777 AccountStorageRequirements::default(),
778 AccountStateAt::Block(block_num),
779 None,
780 None,
781 )
782 .await?;
783 let (witness, _) = account_proof.into_parts();
784 AccountInputs::new(partial_account, witness)
785 },
786 };
787
788 return_foreign_account_inputs.push(foreign_account_inputs);
789 }
790
791 Ok((Some(block_num), return_foreign_account_inputs))
792 }
793
794 async fn prepare_program_execution(
798 &mut self,
799 account_id: AccountId,
800 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
801 ) -> Result<(ClientDataStore, BlockNumber), ClientError> {
802 let (fpi_block_number, foreign_account_inputs) =
803 self.retrieve_foreign_account_inputs(foreign_accounts).await?;
804
805 let block_ref = if let Some(block_number) = fpi_block_number {
806 block_number
807 } else {
808 self.get_sync_height().await?
809 };
810
811 let account_record = self
812 .store
813 .get_account(account_id)
814 .await?
815 .ok_or(ClientError::AccountDataNotFound(account_id))?;
816
817 let account: Account = account_record.try_into()?;
818
819 let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
820
821 data_store.register_foreign_account_inputs(foreign_account_inputs.iter().cloned());
822
823 data_store.mast_store().load_account_code(account.code());
825
826 for fpi_account in &foreign_account_inputs {
827 data_store.mast_store().load_account_code(fpi_account.code());
828 }
829
830 Ok((data_store, block_ref))
831 }
832
833 pub(crate) fn build_executor<'store, 'auth, STORE: DataStore + Sync>(
836 &'auth self,
837 data_store: &'store STORE,
838 ) -> Result<TransactionExecutor<'store, 'auth, STORE, AUTH>, TransactionExecutorError> {
839 let mut executor = TransactionExecutor::new(data_store)
840 .with_options(self.exec_options)?
841 .with_source_manager(self.source_manager.clone());
842 if let Some(authenticator) = self.authenticator.as_deref() {
843 executor = executor.with_authenticator(authenticator);
844 }
845
846 Ok(executor)
847 }
848
849 #[cfg(feature = "dap")]
851 pub(crate) fn build_dap_executor<'store, 'auth, STORE: DataStore + Sync>(
852 &'auth self,
853 data_store: &'store STORE,
854 ) -> Result<
855 TransactionExecutor<'store, 'auth, STORE, AUTH, miden_debug::DapExecutor>,
856 TransactionExecutorError,
857 > {
858 Ok(self
859 .build_executor(data_store)?
860 .with_program_executor::<miden_debug::DapExecutor>())
861 }
862}
863
864fn get_outgoing_assets(
872 transaction_request: &TransactionRequest,
873) -> (BTreeMap<AccountId, u64>, Vec<NonFungibleAsset>) {
874 let mut own_notes_assets = match transaction_request.script_template() {
876 Some(TransactionScriptTemplate::SendNotes(notes)) => notes
877 .iter()
878 .map(|note| (note.id(), note.assets().clone()))
879 .collect::<BTreeMap<_, _>>(),
880 _ => BTreeMap::default(),
881 };
882 let mut output_notes_assets = transaction_request
884 .expected_output_own_notes()
885 .into_iter()
886 .map(|note| (note.id(), note.assets().clone()))
887 .collect::<BTreeMap<_, _>>();
888
889 output_notes_assets.append(&mut own_notes_assets);
891
892 let outgoing_assets = output_notes_assets.values().flat_map(|note_assets| note_assets.iter());
894
895 request::collect_assets(outgoing_assets)
896}
897
898fn validate_basic_account_request(
901 transaction_request: &TransactionRequest,
902 account: &Account,
903) -> Result<(), ClientError> {
904 let (fungible_balance_map, non_fungible_set) = get_outgoing_assets(transaction_request);
906
907 let (incoming_fungible_balance_map, incoming_non_fungible_balance_set) =
909 transaction_request.incoming_assets();
910
911 for (faucet_id, amount) in fungible_balance_map {
914 let account_asset_amount = account.vault().get_balance(faucet_id).unwrap_or(0);
915 let incoming_balance = incoming_fungible_balance_map.get(&faucet_id).unwrap_or(&0);
916 if account_asset_amount + incoming_balance < amount {
917 return Err(ClientError::AssetError(AssetError::FungibleAssetAmountNotSufficient {
918 minuend: account_asset_amount,
919 subtrahend: amount,
920 }));
921 }
922 }
923
924 for non_fungible in &non_fungible_set {
927 match account.vault().has_non_fungible_asset(*non_fungible) {
928 Ok(true) => (),
929 Ok(false) => {
930 if !incoming_non_fungible_balance_set.contains(non_fungible) {
932 return Err(ClientError::AssetError(
933 AssetError::NonFungibleFaucetIdTypeMismatch(non_fungible.faucet_id()),
934 ));
935 }
936 },
937 _ => {
938 return Err(ClientError::AssetError(AssetError::NonFungibleFaucetIdTypeMismatch(
939 non_fungible.faucet_id(),
940 )));
941 },
942 }
943 }
944
945 Ok(())
946}
947
948pub(crate) async fn fetch_public_account_inputs(
955 store: &Arc<dyn Store>,
956 rpc_api: &Arc<dyn NodeRpcClient>,
957 account_id: AccountId,
958 storage_requirements: AccountStorageRequirements,
959 account_state_at: AccountStateAt,
960) -> Result<AccountInputs, ClientError> {
961 let known_account_code: Option<AccountCode> =
962 store.get_foreign_account_code(vec![account_id]).await?.into_values().next();
963
964 let (_, account_proof) = rpc_api
965 .get_account_proof(
966 account_id,
967 storage_requirements.clone(),
968 account_state_at,
969 known_account_code,
970 Some(EMPTY_WORD),
971 )
972 .await?;
973
974 let account_inputs = request::account_proof_into_inputs(account_proof, &storage_requirements)?;
975
976 let _ = store
977 .upsert_foreign_account_code(account_id, account_inputs.code().clone())
978 .await
979 .inspect_err(|err| {
980 tracing::warn!(
981 %account_id,
982 %err,
983 "Failed to persist foreign account code to store"
984 );
985 });
986
987 Ok(account_inputs)
988}
989
990pub fn notes_from_output(output_notes: &RawOutputNotes) -> impl Iterator<Item = &Note> {
995 output_notes.iter().filter_map(|n| match n {
996 RawOutputNote::Full(n) => Some(n),
997 RawOutputNote::Partial(_) => None,
998 })
999}
1000
1001fn validate_executed_transaction(
1004 executed_transaction: &ExecutedTransaction,
1005 expected_output_recipients: &[NoteRecipient],
1006) -> Result<(), ClientError> {
1007 let tx_output_recipient_digests = executed_transaction
1008 .output_notes()
1009 .iter()
1010 .filter_map(|n| n.recipient().map(NoteRecipient::digest))
1011 .collect::<Vec<_>>();
1012
1013 let missing_recipient_digest: Vec<Word> = expected_output_recipients
1014 .iter()
1015 .filter_map(|recipient| {
1016 (!tx_output_recipient_digests.contains(&recipient.digest()))
1017 .then_some(recipient.digest())
1018 })
1019 .collect();
1020
1021 if !missing_recipient_digest.is_empty() {
1022 return Err(ClientError::MissingOutputRecipients(missing_recipient_digest));
1023 }
1024
1025 Ok(())
1026}