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::account::{AccountCodeInterface, AccountCodeUpgrade, AccountId};
10use miden_protocol::asset::Asset;
11use miden_protocol::block::BlockNumber;
12use miden_protocol::crypto::SequentialCommit;
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_protocol::{Felt, Word};
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    /// Returns this request with the auth arg set to the commitment of `auth_args`, and with their
298    /// preimage added to the advice map.
299    ///
300    /// The executing party can use this method to set auth args that only it can choose, for
301    /// example [`MultisigAuthArgs`](miden_standards::account::auth::MultisigAuthArgs) with a salt
302    /// and a bound block of its choice. The new auth args replace the previous auth args and the
303    /// declared fee conversion salt. The client tracks the bound block of multisig auth args.
304    #[must_use]
305    pub fn with_auth_args<A: SequentialCommit<Commitment = Word>>(mut self, auth_args: &A) -> Self {
306        self.commit_auth_args(auth_args.to_commitment(), auth_args.to_elements());
307        self.fee_conversion_salt = None;
308        self
309    }
310
311    /// Sets `auth_arg` and adds its `preimage` to the advice map.
312    pub(crate) fn commit_auth_args(&mut self, auth_arg: Word, preimage: Vec<Felt>) {
313        self.advice_map.insert(auth_arg, preimage);
314        self.auth_arg = Some(auth_arg);
315    }
316
317    /// Checks the invariants every request must hold, whether it was built or deserialized.
318    ///
319    /// # Errors
320    /// - If a note appears more than once among the input notes.
321    fn validate(&self) -> Result<(), TransactionRequestError> {
322        let mut seen_input_notes = BTreeSet::new();
323        for (note_id, _) in &self.input_notes_args {
324            if !seen_input_notes.insert(note_id) {
325                return Err(TransactionRequestError::DuplicateInputNote(*note_id));
326            }
327        }
328
329        Ok(())
330    }
331
332    /// Builds the [`InputNotes`] needed for the transaction execution.
333    ///
334    /// A note with a pinned mode keeps that mode. Any other note is authenticated when
335    /// `authenticated_note_records` holds a record for it and unauthenticated otherwise. The result
336    /// keeps the order of the request.
337    pub(crate) fn build_input_notes(
338        &self,
339        authenticated_note_records: Vec<InputNoteRecord>,
340    ) -> Result<InputNotes<InputNote>, TransactionRequestError> {
341        let mut authenticated_notes: BTreeMap<NoteId, InputNoteRecord> = BTreeMap::new();
342        for record in authenticated_note_records {
343            // Authenticated note records always carry metadata (their inclusion proof injected it),
344            // so `id()` is `Some`.
345            let note_id =
346                record.id().expect("authenticated note record carries metadata so id() is Some");
347
348            if !record.is_authenticated() {
349                return Err(TransactionRequestError::InputNoteNotAuthenticated(note_id));
350            }
351            if record.is_consumed() {
352                return Err(TransactionRequestError::InputNoteAlreadyConsumed(
353                    record.details_commitment(),
354                ));
355            }
356
357            authenticated_notes.insert(note_id, record);
358        }
359
360        let input_notes = self
361            .input_notes()
362            .iter()
363            .map(|note| match self.explicit_input_notes.get(&note.id()) {
364                Some(input_note) => input_note.clone(),
365                None => match authenticated_notes.remove(&note.id()) {
366                    Some(record) => record
367                        .try_into()
368                        .expect("Authenticated note record should be convertible to InputNote"),
369                    None => InputNote::unauthenticated(note.clone()),
370                },
371            })
372            .collect();
373
374        Ok(InputNotes::new(input_notes)?)
375    }
376
377    /// Converts the [`TransactionRequest`] into [`TransactionArgs`] in order to be executed by a
378    /// Miden host.
379    pub(crate) fn into_transaction_args(
380        self,
381        tx_script: Option<(TransactionScript, Option<Word>)>,
382    ) -> TransactionArgs {
383        let note_args = self.get_note_args();
384        let TransactionRequest {
385            expected_output_recipients,
386            advice_map,
387            merkle_store,
388            account_code_upgrade,
389            ..
390        } = self;
391
392        let mut tx_args = TransactionArgs::new(advice_map).with_note_args(note_args);
393
394        // A script argument without a script has nothing to bind to, so it is only applied when a
395        // transaction script is present. With no argument the default empty word is used, which is
396        // equivalent to setting no argument at all.
397        if let Some((tx_script, script_args)) = tx_script {
398            let script_args = script_args.or(self.script_arg).unwrap_or_default();
399            tx_args = tx_args.with_tx_script_and_args(tx_script, script_args);
400        }
401
402        if let Some(auth_argument) = self.auth_arg {
403            tx_args = tx_args.with_auth_args(auth_argument);
404        }
405
406        if let Some(account_code_upgrade) = account_code_upgrade {
407            tx_args = tx_args.with_account_code_upgrade(account_code_upgrade);
408        }
409
410        tx_args
411            .extend_output_note_recipients(expected_output_recipients.into_values().map(Box::new));
412        tx_args.extend_merkle_store(merkle_store.inner_nodes());
413
414        tx_args
415    }
416
417    /// Builds the transaction script based on the account capabilities and the transaction request.
418    ///
419    /// Returns the script together with the `TX_SCRIPT_ARGS` word it must be executed with, if the
420    /// script determines it. The `SendNotes` script reads the notes it creates from the advice
421    /// provider and only receives their payload commitment on the stack, so its argument is fixed
422    /// by the notes the script was built for, and passing anything else makes the script fail to
423    /// resolve its payload. A caller-supplied [`TransactionScriptTemplate::CustomScript`] carries
424    /// no such constraint and yields `None`, so the request's own
425    /// [`TransactionRequestBuilder::script_arg`] applies to it.
426    ///
427    /// A request without a script template normally runs without a transaction script. When such a
428    /// request sets an expiration delta, the standard [`ExpirationTransactionScript`] is used so
429    /// that the delta is enforced; the script reads the delta from its own `TX_SCRIPT_ARGS`.
430    ///
431    /// Scripts supplied by the caller via [`TransactionScriptTemplate::CustomScript`] are expected
432    /// to have already been compiled against the client's source manager (e.g. via
433    /// [`Client::code_builder`](crate::Client::code_builder)).
434    pub(crate) fn build_transaction_script(
435        &self,
436        code_interface: &AccountCodeInterface,
437    ) -> Result<Option<(TransactionScript, Option<Word>)>, TransactionRequestError> {
438        match &self.script_template {
439            Some(TransactionScriptTemplate::CustomScript(script)) => {
440                Ok(Some((script.clone(), None)))
441            },
442            Some(TransactionScriptTemplate::SendNotes(notes)) => {
443                let script = match self.expiration_delta.and_then(NonZeroU16::new) {
444                    Some(delta) => SendNotesTransactionScript::with_expiration_delta(
445                        code_interface,
446                        notes,
447                        delta,
448                    )?,
449                    None => SendNotesTransactionScript::new(code_interface, notes)?,
450                };
451                Ok(Some((script.tx_script().clone(), Some(script.tx_script_args()))))
452            },
453            None => match self.expiration_delta.and_then(NonZeroU16::new) {
454                Some(delta) => {
455                    let script = ExpirationTransactionScript::new(delta);
456                    Ok(Some((script.into(), Some(script.tx_script_args()))))
457                },
458                None => Ok(None),
459            },
460        }
461    }
462}
463
464// SERIALIZATION
465// ================================================================================================
466
467impl Serializable for TransactionRequest {
468    fn write_into<W: ByteWriter>(&self, target: &mut W) {
469        self.block_numbers.write_into(target);
470        self.input_notes.write_into(target);
471        self.input_notes_args.write_into(target);
472        self.explicit_input_notes.write_into(target);
473        match &self.script_template {
474            None => target.write_u8(0),
475            Some(TransactionScriptTemplate::CustomScript(script)) => {
476                target.write_u8(1);
477                script.write_into(target);
478            },
479            Some(TransactionScriptTemplate::SendNotes(notes)) => {
480                target.write_u8(2);
481                notes.write_into(target);
482            },
483        }
484        self.expected_output_recipients.write_into(target);
485        self.expected_future_notes.write_into(target);
486        self.advice_map.write_into(target);
487        self.merkle_store.write_into(target);
488        let foreign_accounts: Vec<_> = self.foreign_accounts.values().cloned().collect();
489        foreign_accounts.write_into(target);
490        self.expiration_delta.write_into(target);
491        target.write_u8(u8::from(self.ignore_invalid_input_notes));
492        self.script_arg.write_into(target);
493        self.auth_arg.write_into(target);
494        self.fee_conversion_salt.write_into(target);
495        self.expected_ntx_scripts.write_into(target);
496        self.account_code_upgrade.write_into(target);
497    }
498}
499
500impl Deserializable for TransactionRequest {
501    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
502        let block_numbers = BTreeSet::<BlockNumber>::read_from(source)?;
503        let input_notes = Vec::<Note>::read_from(source)?;
504        let input_notes_args = Vec::<(NoteId, Option<NoteArgs>)>::read_from(source)?;
505        let explicit_input_notes = BTreeMap::<NoteId, InputNote>::read_from(source)?;
506        for (note_id, input_note) in &explicit_input_notes {
507            if *note_id != input_note.id() || !input_notes.contains(input_note.note()) {
508                return Err(DeserializationError::InvalidValue(format!(
509                    "explicit input note {note_id} does not match a request input note"
510                )));
511            }
512        }
513
514        let script_template = match source.read_u8()? {
515            0 => None,
516            1 => {
517                let transaction_script = TransactionScript::read_from(source)?;
518                Some(TransactionScriptTemplate::CustomScript(transaction_script))
519            },
520            2 => {
521                let notes = Vec::<PartialNote>::read_from(source)?;
522                Some(TransactionScriptTemplate::SendNotes(notes))
523            },
524            _ => {
525                return Err(DeserializationError::InvalidValue(
526                    "Invalid script template type".to_string(),
527                ));
528            },
529        };
530
531        let expected_output_recipients = BTreeMap::<Word, NoteRecipient>::read_from(source)?;
532        let expected_future_notes =
533            BTreeMap::<NoteDetailsCommitment, (NoteDetails, NoteTag)>::read_from(source)?;
534
535        let advice_map = AdviceMap::read_from(source)?;
536        let merkle_store = MerkleStore::read_from(source)?;
537        let mut foreign_accounts = BTreeMap::new();
538        for foreign_account in Vec::<ForeignAccount>::read_from(source)? {
539            foreign_accounts.entry(foreign_account.account_id()).or_insert(foreign_account);
540        }
541        let expiration_delta = Option::<u16>::read_from(source)?;
542        let ignore_invalid_input_notes = source.read_u8()? == 1;
543        let script_arg = Option::<Word>::read_from(source)?;
544        let auth_arg = Option::<Word>::read_from(source)?;
545        let fee_conversion_salt = Option::<Word>::read_from(source)?;
546        let expected_ntx_scripts = Vec::<NoteScript>::read_from(source)?;
547        let account_code_upgrade = Option::<AccountCodeUpgrade>::read_from(source)?;
548
549        let request = TransactionRequest {
550            block_numbers,
551            input_notes,
552            input_notes_args,
553            explicit_input_notes,
554            script_template,
555            expected_output_recipients,
556            expected_future_notes,
557            advice_map,
558            merkle_store,
559            foreign_accounts,
560            expiration_delta,
561            ignore_invalid_input_notes,
562            script_arg,
563            auth_arg,
564            fee_conversion_salt,
565            expected_ntx_scripts,
566            account_code_upgrade,
567        };
568        request
569            .validate()
570            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
571
572        Ok(request)
573    }
574}
575
576// HELPERS
577// ================================================================================================
578
579/// Accumulates fungible totals and collectable non-fungible assets from an iterator of assets.
580///
581/// An asset that is neither fungible nor non-fungible is left out of both buckets, since neither
582/// balance arithmetic applies to it. Execution judges such an asset instead.
583pub(crate) fn collect_assets<'a>(
584    assets: impl Iterator<Item = &'a Asset>,
585) -> (BTreeMap<AccountId, u64>, Vec<Asset>) {
586    let mut fungible_balance_map = BTreeMap::new();
587    let mut non_fungible_set = Vec::new();
588
589    for asset in assets {
590        if let Some(fungible) = asset.as_fungible() {
591            let amount = fungible.amount().as_u64();
592            fungible_balance_map
593                .entry(fungible.faucet_id())
594                .and_modify(|balance| *balance += amount)
595                .or_insert(amount);
596        } else if asset.is_non_fungible() && !non_fungible_set.contains(asset) {
597            non_fungible_set.push(*asset);
598        }
599    }
600
601    (fungible_balance_map, non_fungible_set)
602}
603
604impl Default for TransactionRequestBuilder {
605    fn default() -> Self {
606        Self::new()
607    }
608}
609
610// TRANSACTION REQUEST ERROR
611// ================================================================================================
612
613// Errors related to a [TransactionRequest]
614#[derive(Debug, Error)]
615pub enum TransactionRequestError {
616    #[error("failed to build the send-notes transaction script")]
617    SendNotesTransactionScriptError(#[from] SendNotesTransactionScriptError),
618    #[error("account error")]
619    AccountError(#[from] AccountError),
620    #[error("asset error")]
621    AssetError(#[from] AssetError),
622    #[error("duplicate input note: note {0} was added more than once to the transaction")]
623    DuplicateInputNote(NoteId),
624    #[error("transaction expiration delta must be greater than zero")]
625    ZeroExpirationDelta,
626    #[error(
627        "the account proof does not contain the required foreign account data; re-fetch the proof and retry"
628    )]
629    ForeignAccountDataMissing,
630    #[error(
631        "foreign account {0} has incompatible visibility; use `ForeignAccount::public()` for public accounts and `ForeignAccount::private()` for private accounts"
632    )]
633    InvalidForeignAccountId(AccountId),
634    #[error(
635        "inputs for foreign account {account_id} do not open against the account tree of the \
636         transaction's reference block {block_num}"
637    )]
638    ForeignAccountNotAtReferenceBlock {
639        account_id: AccountId,
640        block_num: BlockNumber,
641    },
642    #[error(
643        "note {0} cannot be used as an authenticated input: it does not have a valid inclusion proof"
644    )]
645    InputNoteNotAuthenticated(NoteId),
646    #[error("note with details commitment {} has already been consumed", .0.to_hex())]
647    InputNoteAlreadyConsumed(NoteDetailsCommitment),
648    #[error(
649        "note with details commitment {} is being consumed by pending transaction {transaction_id}",
650        note.to_hex()
651    )]
652    InputNoteBeingProcessed {
653        note: NoteDetailsCommitment,
654        transaction_id: TransactionId,
655    },
656    #[error(
657        "output note declares sender {actual} but the transaction is executed by account {expected}"
658    )]
659    OutputNoteSenderMismatch { expected: AccountId, actual: AccountId },
660    #[error(
661        "the request declares a fee conversion salt but the account's auth component {0} does not \
662         read the auth args as fee conversion info"
663    )]
664    FeeConversionInfoUnsupported(String),
665    #[error(
666        "account's `{0}` component reuses the fee conversion salt as a replay guard, so the \
667         caller must declare a fresh one with `TransactionRequestBuilder::fee_conversion_salt`"
668    )]
669    FeeConversionInfoRequired(String),
670    #[error("merkle proof error")]
671    MerkleError(#[from] MerkleError),
672    #[error("empty transaction: the request has no input notes and no account state changes")]
673    NoInputNotesNorAccountChange,
674    #[error("failed to create note")]
675    NoteCreationError(#[from] NoteError),
676    #[error("note failed validation")]
677    NoteValidationError(#[source] NoteError),
678    #[error("note execution failed")]
679    NoteExecutionError(#[source] NoteError),
680    #[error("failed to build note args")]
681    NoteArgError(#[source] NoteError),
682    #[error("pay-to-ID note must contain at least one asset to transfer")]
683    P2IDNoteWithoutAsset,
684    #[error("swap note assets must be non-zero: a zero {0} asset makes the exchange one-sided")]
685    SwapNoteWithZeroAsset(&'static str),
686    #[error(
687        "non-fungible asset issued by faucet {0} is not available in the account vault or incoming notes"
688    )]
689    MissingNonFungibleAsset(AccountId),
690    #[error("PSWAP note can only be cancelled by its creator: expected {expected}, got {actual}")]
691    PswapCancelCreatorMismatch { expected: AccountId, actual: AccountId },
692    #[error("error building script")]
693    CodeBuilderError(#[from] CodeBuilderError),
694    #[error("transaction script template error: {0}")]
695    ScriptTemplateError(String),
696    #[error("foreign procedure takes at most {max} input felts, got {actual}")]
697    ForeignProcedureInputsTooLong { max: usize, actual: usize },
698    #[error("storage slot {0} not found in account ID {1}")]
699    StorageSlotNotFound(u8, AccountId),
700    #[error("error while building the input notes")]
701    TransactionInputError(#[from] TransactionInputError),
702    #[error("account storage map error")]
703    StorageMapError(#[from] StorageMapError),
704}
705
706// TESTS
707// ================================================================================================
708
709#[cfg(test)]
710mod tests {
711    use std::vec::Vec;
712
713    use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment};
714    use miden_protocol::account::{
715        AccountBuilder,
716        AccountComponent,
717        AccountId,
718        AccountType,
719        StorageMapKey,
720        StorageSlotName,
721    };
722    use miden_protocol::asset::FungibleAsset;
723    use miden_protocol::block::account_tree::AccountTree;
724    use miden_protocol::crypto::SequentialCommit;
725    use miden_protocol::note::{NoteTag, NoteType};
726    use miden_protocol::testing::account_id::{
727        ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
728        ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
729        ACCOUNT_ID_SENDER,
730    };
731    use miden_protocol::transaction::{AccountInputs, InputNote};
732    use miden_protocol::{EMPTY_WORD, Felt, Word};
733    use miden_standards::account::auth::{Approver, AuthSingleSig, MultisigAuthArgs};
734    use miden_standards::note::P2idNote;
735    use miden_standards::testing::account_component::MockAccountComponent;
736    use miden_tx::utils::serde::{Deserializable, Serializable};
737    use rand::SeedableRng;
738    use rand_chacha::ChaCha20Rng;
739
740    use super::{
741        BlockNumber,
742        ExpirationTransactionScript,
743        NonZeroU16,
744        TransactionRequest,
745        TransactionRequestBuilder,
746        TransactionScript,
747    };
748    use crate::rng::draw_word;
749    use crate::rpc::domain::account::AccountStorageRequirements;
750    use crate::transaction::ForeignAccount;
751
752    #[test]
753    fn transaction_request_serialization() {
754        assert_transaction_request_serialization_with(|| {
755            AuthSingleSig::new(Approver::new(
756                PublicKeyCommitment::from(EMPTY_WORD),
757                AuthScheme::Falcon512Poseidon2,
758            ))
759            .into()
760        });
761    }
762
763    #[test]
764    fn transaction_request_serialization_ecdsa() {
765        assert_transaction_request_serialization_with(|| {
766            AuthSingleSig::new(Approver::new(
767                PublicKeyCommitment::from(EMPTY_WORD),
768                AuthScheme::EcdsaK256Keccak,
769            ))
770            .into()
771        });
772    }
773
774    #[test]
775    fn auth_args_set_the_commitment_and_its_preimage() {
776        let auth_args = MultisigAuthArgs::new(BlockNumber::from(7u32), Word::from([1u32, 2, 3, 4]));
777
778        let request = TransactionRequestBuilder::new()
779            .fee_conversion_salt(Word::from([5u32, 6, 7, 8]))
780            .auth_args(&auth_args)
781            .build()
782            .unwrap();
783
784        let auth_arg = auth_args.to_commitment();
785        assert_eq!(request.auth_arg(), &Some(auth_arg));
786        assert_eq!(request.fee_conversion_salt(), None);
787        assert_eq!(request.advice_map().get(&auth_arg).unwrap().as_ref(), auth_args.to_elements());
788    }
789
790    #[test]
791    fn expiration_delta_without_script_template_builds_expiration_script() {
792        let account = AccountBuilder::new(Default::default())
793            .with_component(MockAccountComponent::with_empty_slots())
794            .with_component(AuthSingleSig::new(Approver::new(
795                PublicKeyCommitment::from(EMPTY_WORD),
796                AuthScheme::Falcon512Poseidon2,
797            )))
798            .account_type(AccountType::Private)
799            .build_existing()
800            .unwrap();
801        let code_interface = account.code_interface();
802
803        let delta = NonZeroU16::new(9).unwrap();
804        let tx_request =
805            TransactionRequestBuilder::new().expiration_delta(delta.get()).build().unwrap();
806
807        let (script, script_args) =
808            tx_request.build_transaction_script(&code_interface).unwrap().unwrap();
809        let expected = ExpirationTransactionScript::new(delta);
810        assert_eq!(script.root(), TransactionScript::from(expected).root());
811        assert_eq!(script_args, Some(expected.tx_script_args()));
812
813        // Without a delta there is still no script to run.
814        let tx_request = TransactionRequestBuilder::new().build().unwrap();
815        assert!(tx_request.build_transaction_script(&code_interface).unwrap().is_none());
816    }
817
818    #[test]
819    fn deserialization_rejects_duplicate_input_notes() {
820        let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
821        let target_id =
822            AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
823        let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
824        let mut rng = ChaCha20Rng::seed_from_u64(0);
825        let note = P2idNote::builder()
826            .sender(sender_id)
827            .target(target_id)
828            .assets(vec![FungibleAsset::new(faucet_id, 100).unwrap()])
829            .note_type(NoteType::Private)
830            .serial_number(draw_word(&mut rng))
831            .build()
832            .unwrap();
833
834        // The builder rejects a duplicate, so the built request is corrupted by hand.
835        let mut tx_request = TransactionRequestBuilder::new()
836            .input_notes(vec![(note.into(), None)])
837            .build()
838            .unwrap();
839        let note_id = tx_request.input_note_ids().next().unwrap();
840        tx_request.input_notes.push(tx_request.input_notes[0].clone());
841        tx_request.input_notes_args.push((note_id, None));
842
843        assert!(TransactionRequest::read_from_bytes(&tx_request.to_bytes()).is_err());
844    }
845
846    #[allow(deprecated)]
847    fn assert_transaction_request_serialization_with<F>(auth_component: F)
848    where
849        F: FnOnce() -> AccountComponent,
850    {
851        let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
852        let target_id =
853            AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
854        let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
855        let mut rng = ChaCha20Rng::seed_from_u64(0);
856
857        let mut notes = vec![];
858        for i in 0..7 {
859            let note = P2idNote::builder()
860                .sender(sender_id)
861                .target(target_id)
862                .assets(vec![FungibleAsset::new(faucet_id, 100 + i).unwrap()])
863                .note_type(NoteType::Private)
864                .serial_number(draw_word(&mut rng))
865                .build()
866                .expect("note creation failed");
867            notes.push(note.into());
868        }
869
870        let mut advice_vec: Vec<(Word, Vec<Felt>)> = vec![];
871        for i in 0u32..10 {
872            advice_vec.push((draw_word(&mut rng), vec![Felt::from(i)]));
873        }
874
875        let account = AccountBuilder::new(Default::default())
876            .with_component(MockAccountComponent::with_empty_slots())
877            .with_component(auth_component())
878            .account_type(AccountType::Private)
879            .build_existing()
880            .unwrap();
881
882        // This transaction request wouldn't be valid in a real scenario, it's intended for testing
883        let tx_request = TransactionRequestBuilder::new()
884            .block_numbers([
885                BlockNumber::from(1u32),
886                BlockNumber::from(3u32),
887                BlockNumber::from(1u32),
888            ])
889            .input_notes(vec![(notes.pop().unwrap(), None)])
890            .explicit_input_notes(vec![(
891                InputNote::unauthenticated(notes.pop().unwrap()),
892                Some(draw_word(&mut rng)),
893            )])
894            .expected_output_recipients(vec![notes.pop().unwrap().recipient().clone()])
895            .expected_future_notes(vec![(
896                notes.pop().unwrap().into(),
897                NoteTag::with_account_target(sender_id),
898            )])
899            .extend_advice_map(advice_vec)
900            .foreign_accounts([
901                ForeignAccount::public(
902                    target_id,
903                    AccountStorageRequirements::new([(
904                        StorageSlotName::new("demo::storage_slot").unwrap(),
905                        &[StorageMapKey::new(Word::default())],
906                    )]),
907                )
908                .unwrap(),
909                ForeignAccount::private(&account).unwrap(),
910            ])
911            .own_output_notes(vec![notes.pop().unwrap(), notes.pop().unwrap()])
912            .script_arg(draw_word(&mut rng))
913            .auth_arg(draw_word(&mut rng))
914            .expected_ntx_scripts(vec![notes.first().unwrap().recipient().script().clone()])
915            .build()
916            .unwrap();
917
918        let mut buffer = Vec::new();
919        tx_request.write_into(&mut buffer);
920
921        let deserialized_tx_request = TransactionRequest::read_from_bytes(&buffer).unwrap();
922        assert_eq!(tx_request, deserialized_tx_request);
923
924        let tree = AccountTree::with_entries([(account.id(), account.to_commitment())]).unwrap();
925        let inputs = AccountInputs::new((&account).into(), tree.open(account.id()));
926        let mut request = tx_request;
927        request.foreign_accounts.insert(inputs.id(), ForeignAccount::Prefetched(inputs));
928        let decoded = TransactionRequest::read_from_bytes(&request.to_bytes()).unwrap();
929        assert_eq!(request, decoded);
930    }
931}