Skip to main content

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