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