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