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