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;
7
8use miden_lib::account::interface::{AccountInterface, AccountInterfaceError};
9use miden_lib::utils::{ScriptBuilder, ScriptBuilderError};
10use miden_objects::account::AccountId;
11use miden_objects::crypto::merkle::{MerkleError, MerkleStore};
12use miden_objects::note::{Note, NoteDetails, NoteId, NoteRecipient, NoteTag, PartialNote};
13use miden_objects::transaction::{
14    AccountInputs,
15    InputNote,
16    InputNotes,
17    TransactionArgs,
18    TransactionScript,
19};
20use miden_objects::vm::AdviceMap;
21use miden_objects::{AccountError, NoteError, TransactionInputError, TransactionScriptError, Word};
22use miden_tx::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable};
23use thiserror::Error;
24
25mod builder;
26pub use builder::{PaymentNoteDescription, SwapTransactionData, TransactionRequestBuilder};
27
28mod foreign;
29pub use foreign::ForeignAccount;
30
31use crate::DebugMode;
32use crate::store::InputNoteRecord;
33
34// TRANSACTION REQUEST
35// ================================================================================================
36
37pub type NoteArgs = Word;
38
39/// Specifies a transaction script to be executed in a transaction.
40///
41/// A transaction script is a program which is executed after scripts of all input notes have been
42/// executed.
43#[derive(Clone, Debug, PartialEq, Eq)]
44pub enum TransactionScriptTemplate {
45    /// Specifies the exact transaction script to be executed in a transaction.
46    CustomScript(TransactionScript),
47    /// Specifies that the transaction script must create the specified output notes.
48    ///
49    /// It is up to the client to determine how the output notes will be created and this will
50    /// depend on the capabilities of the account the transaction request will be applied to.
51    /// For example, for Basic Wallets, this may involve invoking `create_note` procedure.
52    SendNotes(Vec<PartialNote>),
53}
54
55/// Specifies a transaction request that can be executed by an account.
56///
57/// A request contains information about input notes to be consumed by the transaction (if any),
58/// description of the transaction script to be executed (if any), and a set of notes expected
59/// to be generated by the transaction or by consuming notes generated by the transaction.
60#[derive(Clone, Debug, PartialEq, Eq)]
61pub struct TransactionRequest {
62    /// Notes to be consumed by the transaction that aren't authenticated.
63    unauthenticated_input_notes: Vec<Note>,
64    /// Notes to be consumed by the transaction together with their (optional) arguments. This
65    /// includes both authenticated and unauthenticated notes.
66    input_notes: Vec<(NoteId, Option<NoteArgs>)>,
67    /// Template for the creation of the transaction script.
68    script_template: Option<TransactionScriptTemplate>,
69    /// A map of recipients of the output notes expected to be generated by the transaction.
70    expected_output_recipients: BTreeMap<Word, NoteRecipient>,
71    /// A map of details and tags of notes we expect to be created as part of future transactions
72    /// with their respective tags.
73    ///
74    /// For example, after a swap note is consumed, a payback note is expected to be created.
75    expected_future_notes: BTreeMap<NoteId, (NoteDetails, NoteTag)>,
76    /// Initial state of the `AdviceMap` that provides data during runtime.
77    advice_map: AdviceMap,
78    /// Initial state of the `MerkleStore` that provides data during runtime.
79    merkle_store: MerkleStore,
80    /// Foreign account data requirements. At execution time, account data will be retrieved from
81    /// the network, and injected as advice inputs. Additionally, the account's code will be
82    /// added to the executor and prover.
83    foreign_accounts: BTreeSet<ForeignAccount>,
84    /// The number of blocks in relation to the transaction's reference block after which the
85    /// transaction will expire. If `None`, the transaction will not expire.
86    expiration_delta: Option<u16>,
87    /// Indicates whether to **silently** ignore invalid input notes when executing the
88    /// transaction. This will allow the transaction to be executed even if some input notes
89    /// are invalid.
90    ignore_invalid_input_notes: bool,
91    /// Optional [`Word`] that will be pushed to the operand stack before the transaction script
92    /// execution.
93    script_arg: Option<Word>,
94    /// Optional [`Word`] that will be pushed to the stack for the authentication procedure
95    /// during transaction execution.
96    auth_arg: Option<Word>,
97}
98
99impl TransactionRequest {
100    // PUBLIC ACCESSORS
101    // --------------------------------------------------------------------------------------------
102
103    /// Returns a reference to the transaction request's unauthenticated note list.
104    pub fn unauthenticated_input_notes(&self) -> &[Note] {
105        &self.unauthenticated_input_notes
106    }
107
108    /// Returns an iterator over unauthenticated note IDs for the transaction request.
109    pub fn unauthenticated_input_note_ids(&self) -> impl Iterator<Item = NoteId> + '_ {
110        self.unauthenticated_input_notes.iter().map(Note::id)
111    }
112
113    /// Returns an iterator over authenticated input note IDs for the transaction request.
114    pub fn authenticated_input_note_ids(&self) -> impl Iterator<Item = NoteId> + '_ {
115        let unauthenticated_note_ids =
116            self.unauthenticated_input_note_ids().collect::<BTreeSet<_>>();
117
118        self.input_notes().iter().filter_map(move |(note_id, _)| {
119            if unauthenticated_note_ids.contains(note_id) {
120                None
121            } else {
122                Some(*note_id)
123            }
124        })
125    }
126
127    /// Returns the input note IDs and their optional [`NoteArgs`].
128    pub fn input_notes(&self) -> &[(NoteId, Option<NoteArgs>)] {
129        &self.input_notes
130    }
131
132    /// Returns a list of all input note IDs.
133    pub fn get_input_note_ids(&self) -> Vec<NoteId> {
134        self.input_notes.iter().map(|(id, _)| *id).collect()
135    }
136
137    /// Returns a map of note IDs to their respective [`NoteArgs`]. The result will include
138    /// exclusively note IDs for notes for which [`NoteArgs`] have been defined.
139    pub fn get_note_args(&self) -> BTreeMap<NoteId, NoteArgs> {
140        self.input_notes
141            .iter()
142            .filter_map(|(note, args)| args.map(|a| (*note, a)))
143            .collect()
144    }
145
146    /// Returns the expected output own notes of the transaction.
147    ///
148    /// In this context "own notes" refers to notes that are expected to be created directly by the
149    /// transaction script, rather than notes that are created as a result of consuming other
150    /// notes.
151    pub fn expected_output_own_notes(&self) -> Vec<Note> {
152        match &self.script_template {
153            Some(TransactionScriptTemplate::SendNotes(notes)) => notes
154                .iter()
155                .map(|partial| {
156                    Note::new(
157                        partial.assets().clone(),
158                        *partial.metadata(),
159                        self.expected_output_recipients
160                            .get(&partial.recipient_digest())
161                            .expect("Recipient should be included if it's an own note")
162                            .clone(),
163                    )
164                })
165                .collect(),
166            _ => vec![],
167        }
168    }
169
170    /// Returns an iterator over the expected output notes.
171    pub fn expected_output_recipients(&self) -> impl Iterator<Item = &NoteRecipient> {
172        self.expected_output_recipients.values()
173    }
174
175    /// Returns an iterator over expected future notes.
176    pub fn expected_future_notes(&self) -> impl Iterator<Item = &(NoteDetails, NoteTag)> {
177        self.expected_future_notes.values()
178    }
179
180    /// Returns the [`TransactionScriptTemplate`].
181    pub fn script_template(&self) -> &Option<TransactionScriptTemplate> {
182        &self.script_template
183    }
184
185    /// Returns the [`AdviceMap`] for the transaction request.
186    pub fn advice_map(&self) -> &AdviceMap {
187        &self.advice_map
188    }
189
190    /// Returns the [`MerkleStore`] for the transaction request.
191    pub fn merkle_store(&self) -> &MerkleStore {
192        &self.merkle_store
193    }
194
195    /// Returns the IDs of the required foreign accounts for the transaction request.
196    pub fn foreign_accounts(&self) -> &BTreeSet<ForeignAccount> {
197        &self.foreign_accounts
198    }
199
200    /// Returns whether to ignore invalid input notes or not.
201    pub fn ignore_invalid_input_notes(&self) -> bool {
202        self.ignore_invalid_input_notes
203    }
204
205    /// Returns the script argument for the transaction request.
206    pub fn script_arg(&self) -> &Option<Word> {
207        &self.script_arg
208    }
209
210    /// Returns the auth argument for the transaction request.
211    pub fn auth_arg(&self) -> &Option<Word> {
212        &self.auth_arg
213    }
214
215    /// Builds the [`InputNotes`] needed for the transaction execution. Full valid notes for the
216    /// specified authenticated notes need to be provided, otherwise an error will be returned.
217    /// The transaction input notes will include both authenticated and unauthenticated notes in the
218    /// order they were provided in the transaction request.
219    pub(crate) fn build_input_notes(
220        &self,
221        authenticated_note_records: Vec<InputNoteRecord>,
222    ) -> Result<InputNotes<InputNote>, TransactionRequestError> {
223        let mut input_notes: BTreeMap<NoteId, InputNote> = BTreeMap::new();
224
225        // Add provided authenticated input notes to the input notes map.
226        for authenticated_note_record in authenticated_note_records {
227            if !authenticated_note_record.is_authenticated() {
228                return Err(TransactionRequestError::InputNoteNotAuthenticated(
229                    authenticated_note_record.id(),
230                ));
231            }
232
233            if authenticated_note_record.is_consumed() {
234                return Err(TransactionRequestError::InputNoteAlreadyConsumed(
235                    authenticated_note_record.id(),
236                ));
237            }
238
239            input_notes.insert(
240                authenticated_note_record.id(),
241                authenticated_note_record
242                    .try_into()
243                    .expect("Authenticated note record should be convertible to InputNote"),
244            );
245        }
246
247        // Ensure that all authenticated input notes are present in the input notes map before
248        // continuing.
249        for id in self.authenticated_input_note_ids() {
250            if !input_notes.contains_key(&id) {
251                return Err(TransactionRequestError::MissingAuthenticatedInputNote(id));
252            }
253        }
254
255        // Add unauthenticated input notes to the input notes map.
256        for unauthenticated_input_notes in &self.unauthenticated_input_notes {
257            input_notes.insert(
258                unauthenticated_input_notes.id(),
259                InputNote::Unauthenticated {
260                    note: unauthenticated_input_notes.clone(),
261                },
262            );
263        }
264
265        Ok(InputNotes::new(
266            self.get_input_note_ids()
267                .iter()
268                .map(|note_id| {
269                    input_notes
270                        .remove(note_id)
271                        .expect("The input note map was checked to contain all input notes")
272                })
273                .collect(),
274        )?)
275    }
276
277    /// Converts the [`TransactionRequest`] into [`TransactionArgs`] in order to be executed by a
278    /// Miden host.
279    pub(crate) fn into_transaction_args(
280        self,
281        tx_script: TransactionScript,
282        foreign_account_inputs: Vec<AccountInputs>,
283    ) -> TransactionArgs {
284        let note_args = self.get_note_args();
285        let TransactionRequest {
286            expected_output_recipients,
287            advice_map,
288            merkle_store,
289            ..
290        } = self;
291
292        let mut tx_args =
293            TransactionArgs::new(advice_map, foreign_account_inputs).with_note_args(note_args);
294
295        tx_args = if let Some(argument) = self.script_arg {
296            tx_args.with_tx_script_and_args(tx_script, argument)
297        } else {
298            tx_args.with_tx_script(tx_script)
299        };
300
301        if let Some(auth_argument) = self.auth_arg {
302            tx_args = tx_args.with_auth_args(auth_argument);
303        }
304
305        tx_args
306            .extend_output_note_recipients(expected_output_recipients.into_values().map(Box::new));
307        tx_args.extend_merkle_store(merkle_store.inner_nodes());
308
309        tx_args
310    }
311
312    /// Builds the transaction script based on the account capabilities and the transaction request.
313    /// The debug mode enables the script debug logs.
314    pub(crate) fn build_transaction_script(
315        &self,
316        account_interface: &AccountInterface,
317        in_debug_mode: DebugMode,
318    ) -> Result<TransactionScript, TransactionRequestError> {
319        match &self.script_template {
320            Some(TransactionScriptTemplate::CustomScript(script)) => Ok(script.clone()),
321            Some(TransactionScriptTemplate::SendNotes(notes)) => Ok(account_interface
322                .build_send_notes_script(notes, self.expiration_delta, in_debug_mode.into())?),
323            None => {
324                let empty_script = ScriptBuilder::new(true).compile_tx_script("begin nop end")?;
325
326                Ok(empty_script)
327            },
328        }
329    }
330}
331
332// SERIALIZATION
333// ================================================================================================
334
335impl Serializable for TransactionRequest {
336    fn write_into<W: ByteWriter>(&self, target: &mut W) {
337        self.unauthenticated_input_notes.write_into(target);
338        self.input_notes.write_into(target);
339        match &self.script_template {
340            None => target.write_u8(0),
341            Some(TransactionScriptTemplate::CustomScript(script)) => {
342                target.write_u8(1);
343                script.write_into(target);
344            },
345            Some(TransactionScriptTemplate::SendNotes(notes)) => {
346                target.write_u8(2);
347                notes.write_into(target);
348            },
349        }
350        self.expected_output_recipients.write_into(target);
351        self.expected_future_notes.write_into(target);
352        self.advice_map.write_into(target);
353        self.merkle_store.write_into(target);
354        self.foreign_accounts.write_into(target);
355        self.expiration_delta.write_into(target);
356        target.write_u8(u8::from(self.ignore_invalid_input_notes));
357        self.script_arg.write_into(target);
358        self.auth_arg.write_into(target);
359    }
360}
361
362impl Deserializable for TransactionRequest {
363    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
364        let unauthenticated_input_notes = Vec::<Note>::read_from(source)?;
365        let input_notes = Vec::<(NoteId, Option<NoteArgs>)>::read_from(source)?;
366
367        let script_template = match source.read_u8()? {
368            0 => None,
369            1 => {
370                let transaction_script = TransactionScript::read_from(source)?;
371                Some(TransactionScriptTemplate::CustomScript(transaction_script))
372            },
373            2 => {
374                let notes = Vec::<PartialNote>::read_from(source)?;
375                Some(TransactionScriptTemplate::SendNotes(notes))
376            },
377            _ => {
378                return Err(DeserializationError::InvalidValue(
379                    "Invalid script template type".to_string(),
380                ));
381            },
382        };
383
384        let expected_output_recipients = BTreeMap::<Word, NoteRecipient>::read_from(source)?;
385        let expected_future_notes = BTreeMap::<NoteId, (NoteDetails, NoteTag)>::read_from(source)?;
386
387        let advice_map = AdviceMap::read_from(source)?;
388        let merkle_store = MerkleStore::read_from(source)?;
389        let foreign_accounts = BTreeSet::<ForeignAccount>::read_from(source)?;
390        let expiration_delta = Option::<u16>::read_from(source)?;
391        let ignore_invalid_input_notes = source.read_u8()? == 1;
392        let script_arg = Option::<Word>::read_from(source)?;
393        let auth_arg = Option::<Word>::read_from(source)?;
394
395        Ok(TransactionRequest {
396            unauthenticated_input_notes,
397            input_notes,
398            script_template,
399            expected_output_recipients,
400            expected_future_notes,
401            advice_map,
402            merkle_store,
403            foreign_accounts,
404            expiration_delta,
405            ignore_invalid_input_notes,
406            script_arg,
407            auth_arg,
408        })
409    }
410}
411
412impl Default for TransactionRequestBuilder {
413    fn default() -> Self {
414        Self::new()
415    }
416}
417
418// TRANSACTION REQUEST ERROR
419// ================================================================================================
420
421// Errors related to a [TransactionRequest]
422#[derive(Debug, Error)]
423pub enum TransactionRequestError {
424    #[error("account interface error")]
425    AccountInterfaceError(#[from] AccountInterfaceError),
426    #[error("account error")]
427    AccountError(#[from] AccountError),
428    #[error("duplicate input note with IDs: {0}")]
429    DuplicateInputNote(NoteId),
430    #[error("foreign account data missing in the account proof")]
431    ForeignAccountDataMissing,
432    #[error("foreign account storage slot {0} is not a map type")]
433    ForeignAccountStorageSlotInvalidIndex(u8),
434    #[error("requested foreign account with ID {0} does not have an expected storage mode")]
435    InvalidForeignAccountId(AccountId),
436    #[error("note {0} does not contain a valid inclusion proof")]
437    InputNoteNotAuthenticated(NoteId),
438    #[error("note {0} has already been consumed")]
439    InputNoteAlreadyConsumed(NoteId),
440    #[error("own notes shouldn't be of the header variant")]
441    InvalidNoteVariant,
442    #[error("invalid sender account id: {0}")]
443    InvalidSenderAccount(AccountId),
444    #[error("invalid transaction script")]
445    InvalidTransactionScript(#[from] TransactionScriptError),
446    #[error("merkle error")]
447    MerkleError(#[from] MerkleError),
448    #[error("specified authenticated input note with id {0} is missing")]
449    MissingAuthenticatedInputNote(NoteId),
450    #[error("a transaction without output notes must have at least one input note")]
451    NoInputNotes,
452    #[error("note not found: {0}")]
453    NoteNotFound(String),
454    #[error("note creation error")]
455    NoteCreationError(#[from] NoteError),
456    #[error("pay to id note doesn't contain at least one asset")]
457    P2IDNoteWithoutAsset,
458    #[error("error building script: {0}")]
459    ScriptBuilderError(#[from] ScriptBuilderError),
460    #[error("transaction script template error: {0}")]
461    ScriptTemplateError(String),
462    #[error("storage slot {0} not found in account ID {1}")]
463    StorageSlotNotFound(u8, AccountId),
464    #[error("error while building the input notes: {0}")]
465    TransactionInputError(#[from] TransactionInputError),
466}
467
468// TESTS
469// ================================================================================================
470
471#[cfg(test)]
472mod tests {
473    use std::vec::Vec;
474
475    use miden_lib::account::auth::AuthRpoFalcon512;
476    use miden_lib::note::create_p2id_note;
477    use miden_lib::testing::account_component::MockAccountComponent;
478    use miden_objects::account::{AccountBuilder, AccountId, AccountType};
479    use miden_objects::asset::FungibleAsset;
480    use miden_objects::crypto::dsa::rpo_falcon512::PublicKey;
481    use miden_objects::crypto::rand::{FeltRng, RpoRandomCoin};
482    use miden_objects::note::{NoteTag, NoteType};
483    use miden_objects::testing::account_id::{
484        ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
485        ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
486        ACCOUNT_ID_SENDER,
487    };
488    use miden_objects::transaction::OutputNote;
489    use miden_objects::{EMPTY_WORD, Felt, Word, ZERO};
490    use miden_tx::utils::{Deserializable, Serializable};
491
492    use super::{TransactionRequest, TransactionRequestBuilder};
493    use crate::rpc::domain::account::AccountStorageRequirements;
494    use crate::transaction::ForeignAccount;
495
496    #[test]
497    fn transaction_request_serialization() {
498        let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
499        let target_id =
500            AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
501        let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
502        let mut rng = RpoRandomCoin::new(Word::default());
503
504        let mut notes = vec![];
505        for i in 0..6 {
506            let note = create_p2id_note(
507                sender_id,
508                target_id,
509                vec![FungibleAsset::new(faucet_id, 100 + i).unwrap().into()],
510                NoteType::Private,
511                ZERO,
512                &mut rng,
513            )
514            .unwrap();
515            notes.push(note);
516        }
517
518        let mut advice_vec: Vec<(Word, Vec<Felt>)> = vec![];
519        for i in 0..10 {
520            advice_vec.push((rng.draw_word(), vec![Felt::new(i)]));
521        }
522
523        let account = AccountBuilder::new(Default::default())
524            .with_component(MockAccountComponent::with_empty_slots())
525            .with_auth_component(AuthRpoFalcon512::new(PublicKey::new(EMPTY_WORD)))
526            .account_type(AccountType::RegularAccountImmutableCode)
527            .storage_mode(miden_objects::account::AccountStorageMode::Private)
528            .build_existing()
529            .unwrap();
530
531        // This transaction request wouldn't be valid in a real scenario, it's intended for testing
532        let tx_request = TransactionRequestBuilder::new()
533            .authenticated_input_notes(vec![(notes.pop().unwrap().id(), None)])
534            .unauthenticated_input_notes(vec![(notes.pop().unwrap(), None)])
535            .expected_output_recipients(vec![notes.pop().unwrap().recipient().clone()])
536            .expected_future_notes(vec![(
537                notes.pop().unwrap().into(),
538                NoteTag::from_account_id(sender_id),
539            )])
540            .extend_advice_map(advice_vec)
541            .foreign_accounts([
542                ForeignAccount::public(
543                    target_id,
544                    AccountStorageRequirements::new([(5u8, &[Word::default()])]),
545                )
546                .unwrap(),
547                ForeignAccount::private(account).unwrap(),
548            ])
549            .own_output_notes(vec![
550                OutputNote::Full(notes.pop().unwrap()),
551                OutputNote::Partial(notes.pop().unwrap().into()),
552            ])
553            .script_arg(rng.draw_word())
554            .auth_arg(rng.draw_word())
555            .build()
556            .unwrap();
557
558        let mut buffer = Vec::new();
559        tx_request.write_into(&mut buffer);
560
561        let deserialized_tx_request = TransactionRequest::read_from_bytes(&buffer).unwrap();
562        assert_eq!(tx_request, deserialized_tx_request);
563    }
564}