1use alloc::boxed::Box;
4use alloc::collections::{BTreeMap, BTreeSet};
5use alloc::string::{String, ToString};
6use alloc::vec::Vec;
7use core::num::NonZeroU16;
8
9use miden_protocol::Word;
10use miden_protocol::account::{AccountCodeInterface, AccountId};
11use miden_protocol::asset::{Asset, NonFungibleAsset};
12use miden_protocol::block::BlockNumber;
13use miden_protocol::crypto::merkle::MerkleError;
14use miden_protocol::crypto::merkle::store::MerkleStore;
15use miden_protocol::errors::{
16 AccountError,
17 AssetError,
18 AssetVaultError,
19 NoteError,
20 StorageMapError,
21 TransactionInputError,
22 TransactionScriptError,
23};
24use miden_protocol::note::{
25 Note,
26 NoteDetails,
27 NoteDetailsCommitment,
28 NoteId,
29 NoteRecipient,
30 NoteScript,
31 NoteTag,
32 PartialNote,
33};
34use miden_protocol::transaction::{InputNote, InputNotes, TransactionArgs, TransactionScript};
35use miden_protocol::vm::AdviceMap;
36use miden_standards::account::auth::{FeeConversionInfo, commit_fee_conversion_info};
37use miden_standards::errors::CodeBuilderError;
38use miden_standards::tx_script::{SendNotesTransactionScript, SendNotesTransactionScriptError};
39use miden_tx::utils::serde::{
40 ByteReader,
41 ByteWriter,
42 Deserializable,
43 DeserializationError,
44 Serializable,
45};
46use thiserror::Error;
47
48mod builder;
49pub use builder::{
50 PaymentNoteDescription,
51 PswapTransactionData,
52 SwapTransactionData,
53 TransactionRequestBuilder,
54};
55
56mod foreign;
57pub(crate) use foreign::account_proof_into_inputs;
58pub use foreign::{ForeignAccount, build_fpi_script};
59
60use crate::store::InputNoteRecord;
61
62pub type NoteArgs = Word;
66
67#[derive(Clone, Debug, PartialEq, Eq)]
72pub enum TransactionScriptTemplate {
73 CustomScript(TransactionScript),
75 SendNotes(Vec<PartialNote>),
81}
82
83#[derive(Clone, Debug, PartialEq, Eq)]
89pub struct TransactionRequest {
90 input_notes: Vec<Note>,
95 input_notes_args: Vec<(NoteId, Option<NoteArgs>)>,
98 pub(super) explicit_input_notes: BTreeMap<NoteId, InputNote>,
101 script_template: Option<TransactionScriptTemplate>,
103 expected_output_recipients: BTreeMap<Word, NoteRecipient>,
105 expected_future_notes: BTreeMap<NoteDetailsCommitment, (NoteDetails, NoteTag)>,
110 advice_map: AdviceMap,
112 merkle_store: MerkleStore,
114 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
118 expiration_delta: Option<u16>,
121 ignore_invalid_input_notes: bool,
125 script_arg: Option<Word>,
128 auth_arg: Option<Word>,
131 fee_conversion_salt: Option<Word>,
135 expected_ntx_scripts: Vec<NoteScript>,
139}
140
141impl TransactionRequest {
142 pub fn input_notes(&self) -> &[Note] {
147 &self.input_notes
148 }
149
150 pub fn input_note_ids(&self) -> impl Iterator<Item = NoteId> {
152 self.input_notes.iter().map(Note::id)
153 }
154
155 pub fn incoming_assets(&self) -> (BTreeMap<AccountId, u64>, Vec<NonFungibleAsset>) {
157 collect_assets(self.input_notes.iter().flat_map(|note| note.assets().iter()))
158 }
159
160 pub fn get_note_args(&self) -> BTreeMap<NoteId, NoteArgs> {
163 self.input_notes_args
164 .iter()
165 .filter_map(|(note, args)| args.map(|a| (*note, a)))
166 .collect()
167 }
168
169 pub fn expected_output_own_notes(&self) -> Vec<Note> {
175 match &self.script_template {
176 Some(TransactionScriptTemplate::SendNotes(notes)) => notes
177 .iter()
178 .map(|partial| {
179 Note::with_attachments(
180 partial.assets().clone(),
181 *partial.partial_metadata(),
182 self.expected_output_recipients
183 .get(&partial.recipient_digest())
184 .expect("Recipient should be included if it's an own note")
185 .clone(),
186 partial.attachments().clone(),
187 )
188 })
189 .collect(),
190 _ => vec![],
191 }
192 }
193
194 pub fn expected_output_recipients(&self) -> impl Iterator<Item = &NoteRecipient> {
196 self.expected_output_recipients.values()
197 }
198
199 pub fn expected_future_notes(&self) -> impl Iterator<Item = &(NoteDetails, NoteTag)> {
201 self.expected_future_notes.values()
202 }
203
204 pub fn script_template(&self) -> &Option<TransactionScriptTemplate> {
206 &self.script_template
207 }
208
209 pub fn advice_map(&self) -> &AdviceMap {
211 &self.advice_map
212 }
213
214 pub fn advice_map_mut(&mut self) -> &mut AdviceMap {
216 &mut self.advice_map
217 }
218
219 pub fn merkle_store(&self) -> &MerkleStore {
221 &self.merkle_store
222 }
223
224 pub fn foreign_accounts(&self) -> &BTreeMap<AccountId, ForeignAccount> {
226 &self.foreign_accounts
227 }
228
229 pub fn ignore_invalid_input_notes(&self) -> bool {
231 self.ignore_invalid_input_notes
232 }
233
234 pub fn script_arg(&self) -> &Option<Word> {
236 &self.script_arg
237 }
238
239 pub fn auth_arg(&self) -> &Option<Word> {
241 &self.auth_arg
242 }
243
244 pub fn fee_conversion_salt(&self) -> Option<Word> {
248 self.fee_conversion_salt
249 }
250
251 pub fn has_auth_arg(&self) -> bool {
256 self.auth_arg.is_some_and(|auth_arg| auth_arg != Word::empty())
257 }
258
259 pub fn expected_ntx_scripts(&self) -> &[NoteScript] {
261 &self.expected_ntx_scripts
262 }
263
264 #[cfg(feature = "testing")]
272 pub(crate) fn add_unauthenticated_input_note(&mut self, note: Note) {
273 self.input_notes_args.push((note.id(), None));
274 self.input_notes.push(note);
275 }
276
277 pub(crate) fn commit_native_fee_conversion_info(
280 &mut self,
281 fee_faucet_id: AccountId,
282 salt: Word,
283 ) {
284 let (auth_arg, preimage) =
285 commit_fee_conversion_info(FeeConversionInfo::one_to_one(fee_faucet_id), salt);
286 self.advice_map.insert(auth_arg, preimage);
287 self.auth_arg = Some(auth_arg);
288 }
289
290 fn validate(&self) -> Result<(), TransactionRequestError> {
295 let mut seen_input_notes = BTreeSet::new();
296 for (note_id, _) in &self.input_notes_args {
297 if !seen_input_notes.insert(note_id) {
298 return Err(TransactionRequestError::DuplicateInputNote(*note_id));
299 }
300 }
301
302 Ok(())
303 }
304
305 pub(crate) fn build_input_notes(
311 &self,
312 authenticated_note_records: Vec<InputNoteRecord>,
313 ) -> Result<InputNotes<InputNote>, TransactionRequestError> {
314 let mut authenticated_notes: BTreeMap<NoteId, InputNoteRecord> = BTreeMap::new();
315 for record in authenticated_note_records {
316 let note_id =
319 record.id().expect("authenticated note record carries metadata so id() is Some");
320
321 if !record.is_authenticated() {
322 return Err(TransactionRequestError::InputNoteNotAuthenticated(note_id));
323 }
324 if record.is_consumed() {
325 return Err(TransactionRequestError::InputNoteAlreadyConsumed(
326 record.details_commitment(),
327 ));
328 }
329
330 authenticated_notes.insert(note_id, record);
331 }
332
333 let input_notes = self
334 .input_notes()
335 .iter()
336 .map(|note| match self.explicit_input_notes.get(¬e.id()) {
337 Some(input_note) => input_note.clone(),
338 None => match authenticated_notes.remove(¬e.id()) {
339 Some(record) => record
340 .try_into()
341 .expect("Authenticated note record should be convertible to InputNote"),
342 None => InputNote::unauthenticated(note.clone()),
343 },
344 })
345 .collect();
346
347 Ok(InputNotes::new(input_notes)?)
348 }
349
350 pub(crate) fn into_transaction_args(
353 self,
354 tx_script: Option<(TransactionScript, Option<Word>)>,
355 ) -> TransactionArgs {
356 let note_args = self.get_note_args();
357 let TransactionRequest {
358 expected_output_recipients,
359 advice_map,
360 merkle_store,
361 ..
362 } = self;
363
364 let mut tx_args = TransactionArgs::new(advice_map).with_note_args(note_args);
365
366 if let Some((tx_script, script_args)) = tx_script {
370 let script_args = script_args.or(self.script_arg).unwrap_or_default();
371 tx_args = tx_args.with_tx_script_and_args(tx_script, script_args);
372 }
373
374 if let Some(auth_argument) = self.auth_arg {
375 tx_args = tx_args.with_auth_args(auth_argument);
376 }
377
378 tx_args
379 .extend_output_note_recipients(expected_output_recipients.into_values().map(Box::new));
380 tx_args.extend_merkle_store(merkle_store.inner_nodes());
381
382 tx_args
383 }
384
385 pub(crate) fn build_transaction_script(
399 &self,
400 code_interface: &AccountCodeInterface,
401 ) -> Result<Option<(TransactionScript, Option<Word>)>, TransactionRequestError> {
402 match &self.script_template {
403 Some(TransactionScriptTemplate::CustomScript(script)) => {
404 Ok(Some((script.clone(), None)))
405 },
406 Some(TransactionScriptTemplate::SendNotes(notes)) => {
407 let script = match self.expiration_delta.and_then(NonZeroU16::new) {
408 Some(delta) => SendNotesTransactionScript::with_expiration_delta(
409 code_interface,
410 notes,
411 delta,
412 )?,
413 None => SendNotesTransactionScript::new(code_interface, notes)?,
414 };
415 Ok(Some((script.tx_script().clone(), Some(script.tx_script_args()))))
416 },
417 None => Ok(None),
418 }
419 }
420}
421
422impl Serializable for TransactionRequest {
426 fn write_into<W: ByteWriter>(&self, target: &mut W) {
427 self.input_notes.write_into(target);
428 self.input_notes_args.write_into(target);
429 self.explicit_input_notes.write_into(target);
430 match &self.script_template {
431 None => target.write_u8(0),
432 Some(TransactionScriptTemplate::CustomScript(script)) => {
433 target.write_u8(1);
434 script.write_into(target);
435 },
436 Some(TransactionScriptTemplate::SendNotes(notes)) => {
437 target.write_u8(2);
438 notes.write_into(target);
439 },
440 }
441 self.expected_output_recipients.write_into(target);
442 self.expected_future_notes.write_into(target);
443 self.advice_map.write_into(target);
444 self.merkle_store.write_into(target);
445 let foreign_accounts: Vec<_> = self.foreign_accounts.values().cloned().collect();
446 foreign_accounts.write_into(target);
447 self.expiration_delta.write_into(target);
448 target.write_u8(u8::from(self.ignore_invalid_input_notes));
449 self.script_arg.write_into(target);
450 self.auth_arg.write_into(target);
451 self.fee_conversion_salt.write_into(target);
452 self.expected_ntx_scripts.write_into(target);
453 }
454}
455
456impl Deserializable for TransactionRequest {
457 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
458 let input_notes = Vec::<Note>::read_from(source)?;
459 let input_notes_args = Vec::<(NoteId, Option<NoteArgs>)>::read_from(source)?;
460 let explicit_input_notes = BTreeMap::<NoteId, InputNote>::read_from(source)?;
461 for (note_id, input_note) in &explicit_input_notes {
462 if *note_id != input_note.id() || !input_notes.contains(input_note.note()) {
463 return Err(DeserializationError::InvalidValue(format!(
464 "explicit input note {note_id} does not match a request input note"
465 )));
466 }
467 }
468
469 let script_template = match source.read_u8()? {
470 0 => None,
471 1 => {
472 let transaction_script = TransactionScript::read_from(source)?;
473 Some(TransactionScriptTemplate::CustomScript(transaction_script))
474 },
475 2 => {
476 let notes = Vec::<PartialNote>::read_from(source)?;
477 Some(TransactionScriptTemplate::SendNotes(notes))
478 },
479 _ => {
480 return Err(DeserializationError::InvalidValue(
481 "Invalid script template type".to_string(),
482 ));
483 },
484 };
485
486 let expected_output_recipients = BTreeMap::<Word, NoteRecipient>::read_from(source)?;
487 let expected_future_notes =
488 BTreeMap::<NoteDetailsCommitment, (NoteDetails, NoteTag)>::read_from(source)?;
489
490 let advice_map = AdviceMap::read_from(source)?;
491 let merkle_store = MerkleStore::read_from(source)?;
492 let mut foreign_accounts = BTreeMap::new();
493 for foreign_account in Vec::<ForeignAccount>::read_from(source)? {
494 foreign_accounts.entry(foreign_account.account_id()).or_insert(foreign_account);
495 }
496 let expiration_delta = Option::<u16>::read_from(source)?;
497 let ignore_invalid_input_notes = source.read_u8()? == 1;
498 let script_arg = Option::<Word>::read_from(source)?;
499 let auth_arg = Option::<Word>::read_from(source)?;
500 let fee_conversion_salt = Option::<Word>::read_from(source)?;
501 let expected_ntx_scripts = Vec::<NoteScript>::read_from(source)?;
502
503 let request = TransactionRequest {
504 input_notes,
505 input_notes_args,
506 explicit_input_notes,
507 script_template,
508 expected_output_recipients,
509 expected_future_notes,
510 advice_map,
511 merkle_store,
512 foreign_accounts,
513 expiration_delta,
514 ignore_invalid_input_notes,
515 script_arg,
516 auth_arg,
517 fee_conversion_salt,
518 expected_ntx_scripts,
519 };
520 request
521 .validate()
522 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
523
524 Ok(request)
525 }
526}
527
528pub(crate) fn collect_assets<'a>(
533 assets: impl Iterator<Item = &'a Asset>,
534) -> (BTreeMap<AccountId, u64>, Vec<NonFungibleAsset>) {
535 let mut fungible_balance_map = BTreeMap::new();
536 let mut non_fungible_set = Vec::new();
537
538 assets.for_each(|asset| match asset {
539 Asset::Fungible(fungible) => {
540 let amount = fungible.amount().as_u64();
541 fungible_balance_map
542 .entry(fungible.faucet_id())
543 .and_modify(|balance| *balance += amount)
544 .or_insert(amount);
545 },
546 Asset::NonFungible(non_fungible) => {
547 if !non_fungible_set.contains(non_fungible) {
548 non_fungible_set.push(*non_fungible);
549 }
550 },
551 });
552
553 (fungible_balance_map, non_fungible_set)
554}
555
556impl Default for TransactionRequestBuilder {
557 fn default() -> Self {
558 Self::new()
559 }
560}
561
562#[derive(Debug, Error)]
567pub enum TransactionRequestError {
568 #[error("failed to build the send-notes transaction script")]
569 SendNotesTransactionScriptError(#[from] SendNotesTransactionScriptError),
570 #[error("account error")]
571 AccountError(#[from] AccountError),
572 #[error("asset error")]
573 AssetError(#[from] AssetError),
574 #[error("duplicate input note: note {0} was added more than once to the transaction")]
575 DuplicateInputNote(NoteId),
576 #[error("transaction expiration delta must be greater than zero")]
577 ZeroExpirationDelta,
578 #[error(
579 "the account proof does not contain the required foreign account data; re-fetch the proof and retry"
580 )]
581 ForeignAccountDataMissing,
582 #[error(
583 "foreign account {0} has incompatible visibility; use `ForeignAccount::public()` for public accounts and `ForeignAccount::private()` for private accounts"
584 )]
585 InvalidForeignAccountId(AccountId),
586 #[error(
587 "inputs for foreign account {account_id} do not open against the account tree of the \
588 transaction's reference block {block_num}"
589 )]
590 ForeignAccountNotAtReferenceBlock {
591 account_id: AccountId,
592 block_num: BlockNumber,
593 },
594 #[error(
595 "note {0} cannot be used as an authenticated input: it does not have a valid inclusion proof"
596 )]
597 InputNoteNotAuthenticated(NoteId),
598 #[error("note with details commitment {} has already been consumed", .0.to_hex())]
599 InputNoteAlreadyConsumed(NoteDetailsCommitment),
600 #[error(
601 "output note declares sender {actual} but the transaction is executed by account {expected}"
602 )]
603 OutputNoteSenderMismatch { expected: AccountId, actual: AccountId },
604 #[error(
605 "the request declares a fee conversion salt but the account's auth component {0} does not \
606 read the auth args as fee conversion info"
607 )]
608 FeeConversionInfoUnsupported(String),
609 #[error(
610 "account's `{0}` component reuses the fee conversion salt as a replay guard, so the \
611 caller must declare a fresh one with `TransactionRequestBuilder::fee_conversion_salt`"
612 )]
613 FeeConversionInfoRequired(String),
614 #[error("invalid transaction script")]
615 InvalidTransactionScript(#[from] TransactionScriptError),
616 #[error("merkle proof error")]
617 MerkleError(#[from] MerkleError),
618 #[error("empty transaction: the request has no input notes and no account state changes")]
619 NoInputNotesNorAccountChange,
620 #[error("note not found: {0}")]
621 NoteNotFound(String),
622 #[error("failed to create note")]
623 NoteCreationError(#[from] NoteError),
624 #[error("note failed validation")]
625 NoteValidationError(#[source] NoteError),
626 #[error("note execution failed")]
627 NoteExecutionError(#[source] NoteError),
628 #[error("failed to build note args")]
629 NoteArgError(#[source] NoteError),
630 #[error("pay-to-ID note must contain at least one asset to transfer")]
631 P2IDNoteWithoutAsset,
632 #[error(
633 "non-fungible asset issued by faucet {0} is not available in the account vault or incoming notes"
634 )]
635 MissingNonFungibleAsset(AccountId),
636 #[error("PSWAP note can only be cancelled by its creator: expected {expected}, got {actual}")]
637 PswapCancelCreatorMismatch { expected: AccountId, actual: AccountId },
638 #[error("error building script")]
639 CodeBuilderError(#[from] CodeBuilderError),
640 #[error("transaction script template error: {0}")]
641 ScriptTemplateError(String),
642 #[error("foreign procedure takes at most {max} input felts, got {actual}")]
643 ForeignProcedureInputsTooLong { max: usize, actual: usize },
644 #[error("storage slot {0} not found in account ID {1}")]
645 StorageSlotNotFound(u8, AccountId),
646 #[error("error while building the input notes")]
647 TransactionInputError(#[from] TransactionInputError),
648 #[error("account storage map error")]
649 StorageMapError(#[from] StorageMapError),
650 #[error("asset vault error")]
651 AssetVaultError(#[from] AssetVaultError),
652 #[error(
653 "unsupported authentication scheme ID {0}; supported schemes are: RpoFalcon512 (0) and EcdsaK256Keccak (1)"
654 )]
655 UnsupportedAuthSchemeId(u8),
656}
657
658#[cfg(test)]
662mod tests {
663 use std::vec::Vec;
664
665 use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment};
666 use miden_protocol::account::{
667 AccountBuilder,
668 AccountComponent,
669 AccountId,
670 AccountType,
671 StorageMapKey,
672 StorageSlotName,
673 };
674 use miden_protocol::asset::FungibleAsset;
675 use miden_protocol::block::account_tree::AccountTree;
676 use miden_protocol::crypto::rand::{FeltRng, RandomCoin};
677 use miden_protocol::note::{NoteTag, NoteType};
678 use miden_protocol::testing::account_id::{
679 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
680 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
681 ACCOUNT_ID_SENDER,
682 };
683 use miden_protocol::transaction::{AccountInputs, InputNote};
684 use miden_protocol::{EMPTY_WORD, Felt, Word};
685 use miden_standards::account::auth::{Approver, AuthSingleSig};
686 use miden_standards::note::P2idNote;
687 use miden_standards::testing::account_component::MockAccountComponent;
688 use miden_tx::utils::serde::{Deserializable, Serializable};
689
690 use super::{TransactionRequest, TransactionRequestBuilder};
691 use crate::rpc::domain::account::AccountStorageRequirements;
692 use crate::transaction::ForeignAccount;
693
694 #[test]
695 fn transaction_request_serialization() {
696 assert_transaction_request_serialization_with(|| {
697 AuthSingleSig::new(Approver::new(
698 PublicKeyCommitment::from(EMPTY_WORD),
699 AuthScheme::Falcon512Poseidon2,
700 ))
701 .into()
702 });
703 }
704
705 #[test]
706 fn transaction_request_serialization_ecdsa() {
707 assert_transaction_request_serialization_with(|| {
708 AuthSingleSig::new(Approver::new(
709 PublicKeyCommitment::from(EMPTY_WORD),
710 AuthScheme::EcdsaK256Keccak,
711 ))
712 .into()
713 });
714 }
715
716 #[test]
717 fn deserialization_rejects_duplicate_input_notes() {
718 let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
719 let target_id =
720 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
721 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
722 let note = P2idNote::builder()
723 .sender(sender_id)
724 .target(target_id)
725 .assets(vec![FungibleAsset::new(faucet_id, 100).unwrap()])
726 .note_type(NoteType::Private)
727 .generate_serial_number(&mut RandomCoin::new(Word::default()))
728 .build()
729 .unwrap();
730
731 let mut tx_request = TransactionRequestBuilder::new()
733 .input_notes(vec![(note.into(), None)])
734 .build()
735 .unwrap();
736 let note_id = tx_request.input_note_ids().next().unwrap();
737 tx_request.input_notes.push(tx_request.input_notes[0].clone());
738 tx_request.input_notes_args.push((note_id, None));
739
740 assert!(TransactionRequest::read_from_bytes(&tx_request.to_bytes()).is_err());
741 }
742
743 fn assert_transaction_request_serialization_with<F>(auth_component: F)
744 where
745 F: FnOnce() -> AccountComponent,
746 {
747 let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
748 let target_id =
749 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
750 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
751 let mut rng = RandomCoin::new(Word::default());
752
753 let mut notes = vec![];
754 for i in 0..7 {
755 let note = P2idNote::builder()
756 .sender(sender_id)
757 .target(target_id)
758 .assets(vec![FungibleAsset::new(faucet_id, 100 + i).unwrap()])
759 .note_type(NoteType::Private)
760 .generate_serial_number(&mut rng)
761 .build()
762 .expect("note creation failed");
763 notes.push(note.into());
764 }
765
766 let mut advice_vec: Vec<(Word, Vec<Felt>)> = vec![];
767 for i in 0u32..10 {
768 advice_vec.push((rng.draw_word(), vec![Felt::from(i)]));
769 }
770
771 let account = AccountBuilder::new(Default::default())
772 .with_component(MockAccountComponent::with_empty_slots())
773 .with_component(auth_component())
774 .account_type(AccountType::Private)
775 .build_existing()
776 .unwrap();
777
778 let tx_request = TransactionRequestBuilder::new()
780 .input_notes(vec![(notes.pop().unwrap(), None)])
781 .explicit_input_notes(vec![(
782 InputNote::unauthenticated(notes.pop().unwrap()),
783 Some(rng.draw_word()),
784 )])
785 .expected_output_recipients(vec![notes.pop().unwrap().recipient().clone()])
786 .expected_future_notes(vec![(
787 notes.pop().unwrap().into(),
788 NoteTag::with_account_target(sender_id),
789 )])
790 .extend_advice_map(advice_vec)
791 .foreign_accounts([
792 ForeignAccount::public(
793 target_id,
794 AccountStorageRequirements::new([(
795 StorageSlotName::new("demo::storage_slot").unwrap(),
796 &[StorageMapKey::new(Word::default())],
797 )]),
798 )
799 .unwrap(),
800 ForeignAccount::private(&account).unwrap(),
801 ])
802 .own_output_notes(vec![notes.pop().unwrap(), notes.pop().unwrap()])
803 .script_arg(rng.draw_word())
804 .auth_arg(rng.draw_word())
805 .expected_ntx_scripts(vec![notes.first().unwrap().recipient().script().clone()])
806 .build()
807 .unwrap();
808
809 let mut buffer = Vec::new();
810 tx_request.write_into(&mut buffer);
811
812 let deserialized_tx_request = TransactionRequest::read_from_bytes(&buffer).unwrap();
813 assert_eq!(tx_request, deserialized_tx_request);
814
815 let tree = AccountTree::with_entries([(account.id(), account.to_commitment())]).unwrap();
816 let inputs = AccountInputs::new((&account).into(), tree.open(account.id()));
817 let mut request = tx_request;
818 request.foreign_accounts.insert(inputs.id(), ForeignAccount::Prefetched(inputs));
819 let decoded = TransactionRequest::read_from_bytes(&request.to_bytes()).unwrap();
820 assert_eq!(request, decoded);
821 }
822}