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