miden_client/transaction/request/
mod.rs

1//! Contains structures and functions related to transaction creation.
2
3use alloc::boxed::Box;
4use alloc::collections::{BTreeMap, BTreeSet};
5use alloc::string::{String, ToString};
6use alloc::vec::Vec;
7
8use miden_lib::account::interface::{AccountInterface, AccountInterfaceError};
9use miden_lib::utils::{ScriptBuilder, ScriptBuilderError};
10use miden_objects::account::AccountId;
11use miden_objects::crypto::merkle::{MerkleError, MerkleStore};
12use miden_objects::note::{Note, NoteDetails, NoteId, NoteRecipient, NoteTag, PartialNote};
13use miden_objects::transaction::{InputNote, InputNotes, TransactionArgs, TransactionScript};
14use miden_objects::vm::AdviceMap;
15use miden_objects::{
16    AccountError,
17    AssetVaultError,
18    NoteError,
19    StorageMapError,
20    TransactionInputError,
21    TransactionScriptError,
22    Word,
23};
24use miden_tx::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable};
25use thiserror::Error;
26
27mod builder;
28pub use builder::{PaymentNoteDescription, SwapTransactionData, TransactionRequestBuilder};
29
30mod foreign;
31pub use foreign::ForeignAccount;
32
33use crate::DebugMode;
34use crate::store::InputNoteRecord;
35
36// TRANSACTION REQUEST
37// ================================================================================================
38
39pub type NoteArgs = Word;
40
41/// Specifies a transaction script to be executed in a transaction.
42///
43/// A transaction script is a program which is executed after scripts of all input notes have been
44/// executed.
45#[derive(Clone, Debug, PartialEq, Eq)]
46pub enum TransactionScriptTemplate {
47    /// Specifies the exact transaction script to be executed in a transaction.
48    CustomScript(TransactionScript),
49    /// Specifies that the transaction script must create the specified output notes.
50    ///
51    /// It is up to the client to determine how the output notes will be created and this will
52    /// depend on the capabilities of the account the transaction request will be applied to.
53    /// For example, for Basic Wallets, this may involve invoking `create_note` procedure.
54    SendNotes(Vec<PartialNote>),
55}
56
57/// Specifies a transaction request that can be executed by an account.
58///
59/// A request contains information about input notes to be consumed by the transaction (if any),
60/// description of the transaction script to be executed (if any), and a set of notes expected
61/// to be generated by the transaction or by consuming notes generated by the transaction.
62#[derive(Clone, Debug, PartialEq, Eq)]
63pub struct TransactionRequest {
64    /// Notes to be consumed by the transaction that aren't authenticated.
65    unauthenticated_input_notes: Vec<Note>,
66    /// Notes to be consumed by the transaction together with their (optional) arguments. This
67    /// includes both authenticated and unauthenticated notes.
68    input_notes: Vec<(NoteId, Option<NoteArgs>)>,
69    /// Template for the creation of the transaction script.
70    script_template: Option<TransactionScriptTemplate>,
71    /// A map of recipients of the output notes expected to be generated by the transaction.
72    expected_output_recipients: BTreeMap<Word, NoteRecipient>,
73    /// A map of details and tags of notes we expect to be created as part of future transactions
74    /// with their respective tags.
75    ///
76    /// For example, after a swap note is consumed, a payback note is expected to be created.
77    expected_future_notes: BTreeMap<NoteId, (NoteDetails, NoteTag)>,
78    /// Initial state of the `AdviceMap` that provides data during runtime.
79    advice_map: AdviceMap,
80    /// Initial state of the `MerkleStore` that provides data during runtime.
81    merkle_store: MerkleStore,
82    /// Foreign account data requirements. At execution time, account data will be retrieved from
83    /// the network, and injected as advice inputs. Additionally, the account's code will be
84    /// added to the executor and prover.
85    foreign_accounts: BTreeSet<ForeignAccount>,
86    /// The number of blocks in relation to the transaction's reference block after which the
87    /// transaction will expire. If `None`, the transaction will not expire.
88    expiration_delta: Option<u16>,
89    /// Indicates whether to **silently** ignore invalid input notes when executing the
90    /// transaction. This will allow the transaction to be executed even if some input notes
91    /// are invalid.
92    ignore_invalid_input_notes: bool,
93    /// Optional [`Word`] that will be pushed to the operand stack before the transaction script
94    /// execution.
95    script_arg: Option<Word>,
96    /// Optional [`Word`] that will be pushed to the stack for the authentication procedure
97    /// during transaction execution.
98    auth_arg: Option<Word>,
99}
100
101impl TransactionRequest {
102    // PUBLIC ACCESSORS
103    // --------------------------------------------------------------------------------------------
104
105    /// Returns a reference to the transaction request's unauthenticated note list.
106    pub fn unauthenticated_input_notes(&self) -> &[Note] {
107        &self.unauthenticated_input_notes
108    }
109
110    /// Returns an iterator over unauthenticated note IDs for the transaction request.
111    pub fn unauthenticated_input_note_ids(&self) -> impl Iterator<Item = NoteId> + '_ {
112        self.unauthenticated_input_notes.iter().map(Note::id)
113    }
114
115    /// Returns an iterator over authenticated input note IDs for the transaction request.
116    pub fn authenticated_input_note_ids(&self) -> impl Iterator<Item = NoteId> + '_ {
117        let unauthenticated_note_ids =
118            self.unauthenticated_input_note_ids().collect::<BTreeSet<_>>();
119
120        self.input_notes().iter().filter_map(move |(note_id, _)| {
121            if unauthenticated_note_ids.contains(note_id) {
122                None
123            } else {
124                Some(*note_id)
125            }
126        })
127    }
128
129    /// Returns the input note IDs and their optional [`NoteArgs`].
130    pub fn input_notes(&self) -> &[(NoteId, Option<NoteArgs>)] {
131        &self.input_notes
132    }
133
134    /// Returns a list of all input note IDs.
135    pub fn get_input_note_ids(&self) -> Vec<NoteId> {
136        self.input_notes.iter().map(|(id, _)| *id).collect()
137    }
138
139    /// Returns a map of note IDs to their respective [`NoteArgs`]. The result will include
140    /// exclusively note IDs for notes for which [`NoteArgs`] have been defined.
141    pub fn get_note_args(&self) -> BTreeMap<NoteId, NoteArgs> {
142        self.input_notes
143            .iter()
144            .filter_map(|(note, args)| args.map(|a| (*note, a)))
145            .collect()
146    }
147
148    /// Returns the expected output own notes of the transaction.
149    ///
150    /// In this context "own notes" refers to notes that are expected to be created directly by the
151    /// transaction script, rather than notes that are created as a result of consuming other
152    /// notes.
153    pub fn expected_output_own_notes(&self) -> Vec<Note> {
154        match &self.script_template {
155            Some(TransactionScriptTemplate::SendNotes(notes)) => notes
156                .iter()
157                .map(|partial| {
158                    Note::new(
159                        partial.assets().clone(),
160                        *partial.metadata(),
161                        self.expected_output_recipients
162                            .get(&partial.recipient_digest())
163                            .expect("Recipient should be included if it's an own note")
164                            .clone(),
165                    )
166                })
167                .collect(),
168            _ => vec![],
169        }
170    }
171
172    /// Returns an iterator over the expected output notes.
173    pub fn expected_output_recipients(&self) -> impl Iterator<Item = &NoteRecipient> {
174        self.expected_output_recipients.values()
175    }
176
177    /// Returns an iterator over expected future notes.
178    pub fn expected_future_notes(&self) -> impl Iterator<Item = &(NoteDetails, NoteTag)> {
179        self.expected_future_notes.values()
180    }
181
182    /// Returns the [`TransactionScriptTemplate`].
183    pub fn script_template(&self) -> &Option<TransactionScriptTemplate> {
184        &self.script_template
185    }
186
187    /// Returns the [`AdviceMap`] for the transaction request.
188    pub fn advice_map(&self) -> &AdviceMap {
189        &self.advice_map
190    }
191
192    /// Returns a mutable reference to the [`AdviceMap`] for the transaction request.
193    pub fn advice_map_mut(&mut self) -> &mut AdviceMap {
194        &mut self.advice_map
195    }
196
197    /// Returns the [`MerkleStore`] for the transaction request.
198    pub fn merkle_store(&self) -> &MerkleStore {
199        &self.merkle_store
200    }
201
202    /// Returns the IDs of the required foreign accounts for the transaction request.
203    pub fn foreign_accounts(&self) -> &BTreeSet<ForeignAccount> {
204        &self.foreign_accounts
205    }
206
207    /// Returns whether to ignore invalid input notes or not.
208    pub fn ignore_invalid_input_notes(&self) -> bool {
209        self.ignore_invalid_input_notes
210    }
211
212    /// Returns the script argument for the transaction request.
213    pub fn script_arg(&self) -> &Option<Word> {
214        &self.script_arg
215    }
216
217    /// Returns the auth argument for the transaction request.
218    pub fn auth_arg(&self) -> &Option<Word> {
219        &self.auth_arg
220    }
221
222    /// Builds the [`InputNotes`] needed for the transaction execution. Full valid notes for the
223    /// specified authenticated notes need to be provided, otherwise an error will be returned.
224    /// The transaction input notes will include both authenticated and unauthenticated notes in the
225    /// order they were provided in the transaction request.
226    pub(crate) fn build_input_notes(
227        &self,
228        authenticated_note_records: Vec<InputNoteRecord>,
229    ) -> Result<InputNotes<InputNote>, TransactionRequestError> {
230        let mut input_notes: BTreeMap<NoteId, InputNote> = BTreeMap::new();
231
232        // Add provided authenticated input notes to the input notes map.
233        for authenticated_note_record in authenticated_note_records {
234            if !authenticated_note_record.is_authenticated() {
235                return Err(TransactionRequestError::InputNoteNotAuthenticated(
236                    authenticated_note_record.id(),
237                ));
238            }
239
240            if authenticated_note_record.is_consumed() {
241                return Err(TransactionRequestError::InputNoteAlreadyConsumed(
242                    authenticated_note_record.id(),
243                ));
244            }
245
246            input_notes.insert(
247                authenticated_note_record.id(),
248                authenticated_note_record
249                    .try_into()
250                    .expect("Authenticated note record should be convertible to InputNote"),
251            );
252        }
253
254        // Ensure that all authenticated input notes are present in the input notes map before
255        // continuing.
256        for id in self.authenticated_input_note_ids() {
257            if !input_notes.contains_key(&id) {
258                return Err(TransactionRequestError::MissingAuthenticatedInputNote(id));
259            }
260        }
261
262        // Add unauthenticated input notes to the input notes map.
263        for unauthenticated_input_notes in &self.unauthenticated_input_notes {
264            input_notes.insert(
265                unauthenticated_input_notes.id(),
266                InputNote::Unauthenticated {
267                    note: unauthenticated_input_notes.clone(),
268                },
269            );
270        }
271
272        Ok(InputNotes::new(
273            self.get_input_note_ids()
274                .iter()
275                .map(|note_id| {
276                    input_notes
277                        .remove(note_id)
278                        .expect("The input note map was checked to contain all input notes")
279                })
280                .collect(),
281        )?)
282    }
283
284    /// Converts the [`TransactionRequest`] into [`TransactionArgs`] in order to be executed by a
285    /// Miden host.
286    pub(crate) fn into_transaction_args(self, tx_script: TransactionScript) -> TransactionArgs {
287        let note_args = self.get_note_args();
288        let TransactionRequest {
289            expected_output_recipients,
290            advice_map,
291            merkle_store,
292            ..
293        } = self;
294
295        let mut tx_args = TransactionArgs::new(advice_map).with_note_args(note_args);
296
297        tx_args = if let Some(argument) = self.script_arg {
298            tx_args.with_tx_script_and_args(tx_script, argument)
299        } else {
300            tx_args.with_tx_script(tx_script)
301        };
302
303        if let Some(auth_argument) = self.auth_arg {
304            tx_args = tx_args.with_auth_args(auth_argument);
305        }
306
307        tx_args
308            .extend_output_note_recipients(expected_output_recipients.into_values().map(Box::new));
309        tx_args.extend_merkle_store(merkle_store.inner_nodes());
310
311        tx_args
312    }
313
314    /// Builds the transaction script based on the account capabilities and the transaction request.
315    /// The debug mode enables the script debug logs.
316    pub(crate) fn build_transaction_script(
317        &self,
318        account_interface: &AccountInterface,
319        in_debug_mode: DebugMode,
320    ) -> Result<TransactionScript, TransactionRequestError> {
321        match &self.script_template {
322            Some(TransactionScriptTemplate::CustomScript(script)) => Ok(script.clone()),
323            Some(TransactionScriptTemplate::SendNotes(notes)) => Ok(account_interface
324                .build_send_notes_script(notes, self.expiration_delta, in_debug_mode.into())?),
325            None => {
326                let empty_script = ScriptBuilder::new(true).compile_tx_script("begin nop end")?;
327
328                Ok(empty_script)
329            },
330        }
331    }
332}
333
334// SERIALIZATION
335// ================================================================================================
336
337impl Serializable for TransactionRequest {
338    fn write_into<W: ByteWriter>(&self, target: &mut W) {
339        self.unauthenticated_input_notes.write_into(target);
340        self.input_notes.write_into(target);
341        match &self.script_template {
342            None => target.write_u8(0),
343            Some(TransactionScriptTemplate::CustomScript(script)) => {
344                target.write_u8(1);
345                script.write_into(target);
346            },
347            Some(TransactionScriptTemplate::SendNotes(notes)) => {
348                target.write_u8(2);
349                notes.write_into(target);
350            },
351        }
352        self.expected_output_recipients.write_into(target);
353        self.expected_future_notes.write_into(target);
354        self.advice_map.write_into(target);
355        self.merkle_store.write_into(target);
356        self.foreign_accounts.write_into(target);
357        self.expiration_delta.write_into(target);
358        target.write_u8(u8::from(self.ignore_invalid_input_notes));
359        self.script_arg.write_into(target);
360        self.auth_arg.write_into(target);
361    }
362}
363
364impl Deserializable for TransactionRequest {
365    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
366        let unauthenticated_input_notes = Vec::<Note>::read_from(source)?;
367        let input_notes = Vec::<(NoteId, Option<NoteArgs>)>::read_from(source)?;
368
369        let script_template = match source.read_u8()? {
370            0 => None,
371            1 => {
372                let transaction_script = TransactionScript::read_from(source)?;
373                Some(TransactionScriptTemplate::CustomScript(transaction_script))
374            },
375            2 => {
376                let notes = Vec::<PartialNote>::read_from(source)?;
377                Some(TransactionScriptTemplate::SendNotes(notes))
378            },
379            _ => {
380                return Err(DeserializationError::InvalidValue(
381                    "Invalid script template type".to_string(),
382                ));
383            },
384        };
385
386        let expected_output_recipients = BTreeMap::<Word, NoteRecipient>::read_from(source)?;
387        let expected_future_notes = BTreeMap::<NoteId, (NoteDetails, NoteTag)>::read_from(source)?;
388
389        let advice_map = AdviceMap::read_from(source)?;
390        let merkle_store = MerkleStore::read_from(source)?;
391        let foreign_accounts = BTreeSet::<ForeignAccount>::read_from(source)?;
392        let expiration_delta = Option::<u16>::read_from(source)?;
393        let ignore_invalid_input_notes = source.read_u8()? == 1;
394        let script_arg = Option::<Word>::read_from(source)?;
395        let auth_arg = Option::<Word>::read_from(source)?;
396
397        Ok(TransactionRequest {
398            unauthenticated_input_notes,
399            input_notes,
400            script_template,
401            expected_output_recipients,
402            expected_future_notes,
403            advice_map,
404            merkle_store,
405            foreign_accounts,
406            expiration_delta,
407            ignore_invalid_input_notes,
408            script_arg,
409            auth_arg,
410        })
411    }
412}
413
414impl Default for TransactionRequestBuilder {
415    fn default() -> Self {
416        Self::new()
417    }
418}
419
420// TRANSACTION REQUEST ERROR
421// ================================================================================================
422
423// Errors related to a [TransactionRequest]
424#[derive(Debug, Error)]
425pub enum TransactionRequestError {
426    #[error("account interface error")]
427    AccountInterfaceError(#[from] AccountInterfaceError),
428    #[error("account error")]
429    AccountError(#[from] AccountError),
430    #[error("duplicate input note with IDs: {0}")]
431    DuplicateInputNote(NoteId),
432    #[error("foreign account data missing in the account proof")]
433    ForeignAccountDataMissing,
434    #[error("foreign account storage slot {0} is not a map type")]
435    ForeignAccountStorageSlotInvalidIndex(u8),
436    #[error("requested foreign account with ID {0} does not have an expected storage mode")]
437    InvalidForeignAccountId(AccountId),
438    #[error("note {0} does not contain a valid inclusion proof")]
439    InputNoteNotAuthenticated(NoteId),
440    #[error("note {0} has already been consumed")]
441    InputNoteAlreadyConsumed(NoteId),
442    #[error("own notes shouldn't be of the header variant")]
443    InvalidNoteVariant,
444    #[error("invalid sender account id: {0}")]
445    InvalidSenderAccount(AccountId),
446    #[error("invalid transaction script")]
447    InvalidTransactionScript(#[from] TransactionScriptError),
448    #[error("merkle error")]
449    MerkleError(#[from] MerkleError),
450    #[error("specified authenticated input note with id {0} is missing")]
451    MissingAuthenticatedInputNote(NoteId),
452    #[error("a transaction without output notes must have at least one input note")]
453    NoInputNotesNorAccountChange,
454    #[error("note not found: {0}")]
455    NoteNotFound(String),
456    #[error("note creation error")]
457    NoteCreationError(#[from] NoteError),
458    #[error("pay to id note doesn't contain at least one asset")]
459    P2IDNoteWithoutAsset,
460    #[error("error building script: {0}")]
461    ScriptBuilderError(#[from] ScriptBuilderError),
462    #[error("transaction script template error: {0}")]
463    ScriptTemplateError(String),
464    #[error("storage slot {0} not found in account ID {1}")]
465    StorageSlotNotFound(u8, AccountId),
466    #[error("error while building the input notes: {0}")]
467    TransactionInputError(#[from] TransactionInputError),
468    #[error("storage map error")]
469    StorageMapError(#[from] StorageMapError),
470    #[error("asset vault error")]
471    AssetVaultError(#[from] AssetVaultError),
472    #[error("unsupported authentication scheme ID: {0}")]
473    UnsupportedAuthSchemeId(u8),
474}
475
476// TESTS
477// ================================================================================================
478
479#[cfg(test)]
480mod tests {
481    use std::vec::Vec;
482
483    use miden_lib::account::auth::{AuthEcdsaK256Keccak, AuthRpoFalcon512};
484    use miden_lib::note::create_p2id_note;
485    use miden_lib::testing::account_component::MockAccountComponent;
486    use miden_objects::account::auth::PublicKeyCommitment;
487    use miden_objects::account::{AccountBuilder, AccountId, AccountType};
488    use miden_objects::asset::FungibleAsset;
489    use miden_objects::crypto::rand::{FeltRng, RpoRandomCoin};
490    use miden_objects::note::{NoteTag, NoteType};
491    use miden_objects::testing::account_id::{
492        ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
493        ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
494        ACCOUNT_ID_SENDER,
495    };
496    use miden_objects::transaction::OutputNote;
497    use miden_objects::{EMPTY_WORD, Felt, Word, ZERO};
498    use miden_tx::utils::{Deserializable, Serializable};
499
500    use super::{TransactionRequest, TransactionRequestBuilder};
501    use crate::rpc::domain::account::AccountStorageRequirements;
502    use crate::transaction::ForeignAccount;
503
504    #[test]
505    fn transaction_request_serialization() {
506        let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
507        let target_id =
508            AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
509        let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
510        let mut rng = RpoRandomCoin::new(Word::default());
511
512        let mut notes = vec![];
513        for i in 0..6 {
514            let note = create_p2id_note(
515                sender_id,
516                target_id,
517                vec![FungibleAsset::new(faucet_id, 100 + i).unwrap().into()],
518                NoteType::Private,
519                ZERO,
520                &mut rng,
521            )
522            .unwrap();
523            notes.push(note);
524        }
525
526        let mut advice_vec: Vec<(Word, Vec<Felt>)> = vec![];
527        for i in 0..10 {
528            advice_vec.push((rng.draw_word(), vec![Felt::new(i)]));
529        }
530
531        let account = AccountBuilder::new(Default::default())
532            .with_component(MockAccountComponent::with_empty_slots())
533            .with_auth_component(AuthRpoFalcon512::new(PublicKeyCommitment::from(EMPTY_WORD)))
534            .account_type(AccountType::RegularAccountImmutableCode)
535            .storage_mode(miden_objects::account::AccountStorageMode::Private)
536            .build_existing()
537            .unwrap();
538
539        // This transaction request wouldn't be valid in a real scenario, it's intended for testing
540        let tx_request = TransactionRequestBuilder::new()
541            .authenticated_input_notes(vec![(notes.pop().unwrap().id(), None)])
542            .unauthenticated_input_notes(vec![(notes.pop().unwrap(), None)])
543            .expected_output_recipients(vec![notes.pop().unwrap().recipient().clone()])
544            .expected_future_notes(vec![(
545                notes.pop().unwrap().into(),
546                NoteTag::from_account_id(sender_id),
547            )])
548            .extend_advice_map(advice_vec)
549            .foreign_accounts([
550                ForeignAccount::public(
551                    target_id,
552                    AccountStorageRequirements::new([(5u8, &[Word::default()])]),
553                )
554                .unwrap(),
555                ForeignAccount::private(&account).unwrap(),
556            ])
557            .own_output_notes(vec![
558                OutputNote::Full(notes.pop().unwrap()),
559                OutputNote::Partial(notes.pop().unwrap().into()),
560            ])
561            .script_arg(rng.draw_word())
562            .auth_arg(rng.draw_word())
563            .build()
564            .unwrap();
565
566        let mut buffer = Vec::new();
567        tx_request.write_into(&mut buffer);
568
569        let deserialized_tx_request = TransactionRequest::read_from_bytes(&buffer).unwrap();
570        assert_eq!(tx_request, deserialized_tx_request);
571    }
572
573    #[test]
574    fn transaction_request_serialization_ecdsa() {
575        let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
576        let target_id =
577            AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
578        let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
579        let mut rng = RpoRandomCoin::new(Word::default());
580
581        let mut notes = vec![];
582        for i in 0..6 {
583            let note = create_p2id_note(
584                sender_id,
585                target_id,
586                vec![FungibleAsset::new(faucet_id, 100 + i).unwrap().into()],
587                NoteType::Private,
588                ZERO,
589                &mut rng,
590            )
591            .unwrap();
592            notes.push(note);
593        }
594
595        let mut advice_vec: Vec<(Word, Vec<Felt>)> = vec![];
596        for i in 0..10 {
597            advice_vec.push((rng.draw_word(), vec![Felt::new(i)]));
598        }
599
600        let account = AccountBuilder::new(Default::default())
601            .with_component(MockAccountComponent::with_empty_slots())
602            .with_auth_component(AuthEcdsaK256Keccak::new(PublicKeyCommitment::from(EMPTY_WORD)))
603            .account_type(AccountType::RegularAccountImmutableCode)
604            .storage_mode(miden_objects::account::AccountStorageMode::Private)
605            .build_existing()
606            .unwrap();
607
608        // This transaction request wouldn't be valid in a real scenario, it's intended for testing
609        let tx_request = TransactionRequestBuilder::new()
610            .authenticated_input_notes(vec![(notes.pop().unwrap().id(), None)])
611            .unauthenticated_input_notes(vec![(notes.pop().unwrap(), None)])
612            .expected_output_recipients(vec![notes.pop().unwrap().recipient().clone()])
613            .expected_future_notes(vec![(
614                notes.pop().unwrap().into(),
615                NoteTag::from_account_id(sender_id),
616            )])
617            .extend_advice_map(advice_vec)
618            .foreign_accounts([
619                ForeignAccount::public(
620                    target_id,
621                    AccountStorageRequirements::new([(5u8, &[Word::default()])]),
622                )
623                .unwrap(),
624                ForeignAccount::private(&account).unwrap(),
625            ])
626            .own_output_notes(vec![
627                OutputNote::Full(notes.pop().unwrap()),
628                OutputNote::Partial(notes.pop().unwrap().into()),
629            ])
630            .script_arg(rng.draw_word())
631            .auth_arg(rng.draw_word())
632            .build()
633            .unwrap();
634
635        let mut buffer = Vec::new();
636        tx_request.write_into(&mut buffer);
637
638        let deserialized_tx_request = TransactionRequest::read_from_bytes(&buffer).unwrap();
639        assert_eq!(tx_request, deserialized_tx_request);
640    }
641}