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