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