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 a mutable reference to the [`AdviceMap`] for the transaction request.
191    pub fn advice_map_mut(&mut self) -> &mut AdviceMap {
192        &mut self.advice_map
193    }
194
195    /// Returns the [`MerkleStore`] for the transaction request.
196    pub fn merkle_store(&self) -> &MerkleStore {
197        &self.merkle_store
198    }
199
200    /// Returns the IDs of the required foreign accounts for the transaction request.
201    pub fn foreign_accounts(&self) -> &BTreeSet<ForeignAccount> {
202        &self.foreign_accounts
203    }
204
205    /// Returns whether to ignore invalid input notes or not.
206    pub fn ignore_invalid_input_notes(&self) -> bool {
207        self.ignore_invalid_input_notes
208    }
209
210    /// Returns the script argument for the transaction request.
211    pub fn script_arg(&self) -> &Option<Word> {
212        &self.script_arg
213    }
214
215    /// Returns the auth argument for the transaction request.
216    pub fn auth_arg(&self) -> &Option<Word> {
217        &self.auth_arg
218    }
219
220    /// Builds the [`InputNotes`] needed for the transaction execution. Full valid notes for the
221    /// specified authenticated notes need to be provided, otherwise an error will be returned.
222    /// The transaction input notes will include both authenticated and unauthenticated notes in the
223    /// order they were provided in the transaction request.
224    pub(crate) fn build_input_notes(
225        &self,
226        authenticated_note_records: Vec<InputNoteRecord>,
227    ) -> Result<InputNotes<InputNote>, TransactionRequestError> {
228        let mut input_notes: BTreeMap<NoteId, InputNote> = BTreeMap::new();
229
230        // Add provided authenticated input notes to the input notes map.
231        for authenticated_note_record in authenticated_note_records {
232            if !authenticated_note_record.is_authenticated() {
233                return Err(TransactionRequestError::InputNoteNotAuthenticated(
234                    authenticated_note_record.id(),
235                ));
236            }
237
238            if authenticated_note_record.is_consumed() {
239                return Err(TransactionRequestError::InputNoteAlreadyConsumed(
240                    authenticated_note_record.id(),
241                ));
242            }
243
244            input_notes.insert(
245                authenticated_note_record.id(),
246                authenticated_note_record
247                    .try_into()
248                    .expect("Authenticated note record should be convertible to InputNote"),
249            );
250        }
251
252        // Ensure that all authenticated input notes are present in the input notes map before
253        // continuing.
254        for id in self.authenticated_input_note_ids() {
255            if !input_notes.contains_key(&id) {
256                return Err(TransactionRequestError::MissingAuthenticatedInputNote(id));
257            }
258        }
259
260        // Add unauthenticated input notes to the input notes map.
261        for unauthenticated_input_notes in &self.unauthenticated_input_notes {
262            input_notes.insert(
263                unauthenticated_input_notes.id(),
264                InputNote::Unauthenticated {
265                    note: unauthenticated_input_notes.clone(),
266                },
267            );
268        }
269
270        Ok(InputNotes::new(
271            self.get_input_note_ids()
272                .iter()
273                .map(|note_id| {
274                    input_notes
275                        .remove(note_id)
276                        .expect("The input note map was checked to contain all input notes")
277                })
278                .collect(),
279        )?)
280    }
281
282    /// Converts the [`TransactionRequest`] into [`TransactionArgs`] in order to be executed by a
283    /// Miden host.
284    pub(crate) fn into_transaction_args(
285        self,
286        tx_script: TransactionScript,
287        foreign_account_inputs: Vec<AccountInputs>,
288    ) -> TransactionArgs {
289        let note_args = self.get_note_args();
290        let TransactionRequest {
291            expected_output_recipients,
292            advice_map,
293            merkle_store,
294            ..
295        } = self;
296
297        let mut tx_args =
298            TransactionArgs::new(advice_map, foreign_account_inputs).with_note_args(note_args);
299
300        tx_args = if let Some(argument) = self.script_arg {
301            tx_args.with_tx_script_and_args(tx_script, argument)
302        } else {
303            tx_args.with_tx_script(tx_script)
304        };
305
306        if let Some(auth_argument) = self.auth_arg {
307            tx_args = tx_args.with_auth_args(auth_argument);
308        }
309
310        tx_args
311            .extend_output_note_recipients(expected_output_recipients.into_values().map(Box::new));
312        tx_args.extend_merkle_store(merkle_store.inner_nodes());
313
314        tx_args
315    }
316
317    /// Builds the transaction script based on the account capabilities and the transaction request.
318    /// The debug mode enables the script debug logs.
319    pub(crate) fn build_transaction_script(
320        &self,
321        account_interface: &AccountInterface,
322        in_debug_mode: DebugMode,
323    ) -> Result<TransactionScript, TransactionRequestError> {
324        match &self.script_template {
325            Some(TransactionScriptTemplate::CustomScript(script)) => Ok(script.clone()),
326            Some(TransactionScriptTemplate::SendNotes(notes)) => Ok(account_interface
327                .build_send_notes_script(notes, self.expiration_delta, in_debug_mode.into())?),
328            None => {
329                let empty_script = ScriptBuilder::new(true).compile_tx_script("begin nop end")?;
330
331                Ok(empty_script)
332            },
333        }
334    }
335}
336
337// SERIALIZATION
338// ================================================================================================
339
340impl Serializable for TransactionRequest {
341    fn write_into<W: ByteWriter>(&self, target: &mut W) {
342        self.unauthenticated_input_notes.write_into(target);
343        self.input_notes.write_into(target);
344        match &self.script_template {
345            None => target.write_u8(0),
346            Some(TransactionScriptTemplate::CustomScript(script)) => {
347                target.write_u8(1);
348                script.write_into(target);
349            },
350            Some(TransactionScriptTemplate::SendNotes(notes)) => {
351                target.write_u8(2);
352                notes.write_into(target);
353            },
354        }
355        self.expected_output_recipients.write_into(target);
356        self.expected_future_notes.write_into(target);
357        self.advice_map.write_into(target);
358        self.merkle_store.write_into(target);
359        self.foreign_accounts.write_into(target);
360        self.expiration_delta.write_into(target);
361        target.write_u8(u8::from(self.ignore_invalid_input_notes));
362        self.script_arg.write_into(target);
363        self.auth_arg.write_into(target);
364    }
365}
366
367impl Deserializable for TransactionRequest {
368    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
369        let unauthenticated_input_notes = Vec::<Note>::read_from(source)?;
370        let input_notes = Vec::<(NoteId, Option<NoteArgs>)>::read_from(source)?;
371
372        let script_template = match source.read_u8()? {
373            0 => None,
374            1 => {
375                let transaction_script = TransactionScript::read_from(source)?;
376                Some(TransactionScriptTemplate::CustomScript(transaction_script))
377            },
378            2 => {
379                let notes = Vec::<PartialNote>::read_from(source)?;
380                Some(TransactionScriptTemplate::SendNotes(notes))
381            },
382            _ => {
383                return Err(DeserializationError::InvalidValue(
384                    "Invalid script template type".to_string(),
385                ));
386            },
387        };
388
389        let expected_output_recipients = BTreeMap::<Word, NoteRecipient>::read_from(source)?;
390        let expected_future_notes = BTreeMap::<NoteId, (NoteDetails, NoteTag)>::read_from(source)?;
391
392        let advice_map = AdviceMap::read_from(source)?;
393        let merkle_store = MerkleStore::read_from(source)?;
394        let foreign_accounts = BTreeSet::<ForeignAccount>::read_from(source)?;
395        let expiration_delta = Option::<u16>::read_from(source)?;
396        let ignore_invalid_input_notes = source.read_u8()? == 1;
397        let script_arg = Option::<Word>::read_from(source)?;
398        let auth_arg = Option::<Word>::read_from(source)?;
399
400        Ok(TransactionRequest {
401            unauthenticated_input_notes,
402            input_notes,
403            script_template,
404            expected_output_recipients,
405            expected_future_notes,
406            advice_map,
407            merkle_store,
408            foreign_accounts,
409            expiration_delta,
410            ignore_invalid_input_notes,
411            script_arg,
412            auth_arg,
413        })
414    }
415}
416
417impl Default for TransactionRequestBuilder {
418    fn default() -> Self {
419        Self::new()
420    }
421}
422
423// TRANSACTION REQUEST ERROR
424// ================================================================================================
425
426// Errors related to a [TransactionRequest]
427#[derive(Debug, Error)]
428pub enum TransactionRequestError {
429    #[error("account interface error")]
430    AccountInterfaceError(#[from] AccountInterfaceError),
431    #[error("account error")]
432    AccountError(#[from] AccountError),
433    #[error("duplicate input note with IDs: {0}")]
434    DuplicateInputNote(NoteId),
435    #[error("foreign account data missing in the account proof")]
436    ForeignAccountDataMissing,
437    #[error("foreign account storage slot {0} is not a map type")]
438    ForeignAccountStorageSlotInvalidIndex(u8),
439    #[error("requested foreign account with ID {0} does not have an expected storage mode")]
440    InvalidForeignAccountId(AccountId),
441    #[error("note {0} does not contain a valid inclusion proof")]
442    InputNoteNotAuthenticated(NoteId),
443    #[error("note {0} has already been consumed")]
444    InputNoteAlreadyConsumed(NoteId),
445    #[error("own notes shouldn't be of the header variant")]
446    InvalidNoteVariant,
447    #[error("invalid sender account id: {0}")]
448    InvalidSenderAccount(AccountId),
449    #[error("invalid transaction script")]
450    InvalidTransactionScript(#[from] TransactionScriptError),
451    #[error("merkle error")]
452    MerkleError(#[from] MerkleError),
453    #[error("specified authenticated input note with id {0} is missing")]
454    MissingAuthenticatedInputNote(NoteId),
455    #[error("a transaction without output notes must have at least one input note")]
456    NoInputNotes,
457    #[error("note not found: {0}")]
458    NoteNotFound(String),
459    #[error("note creation error")]
460    NoteCreationError(#[from] NoteError),
461    #[error("pay to id note doesn't contain at least one asset")]
462    P2IDNoteWithoutAsset,
463    #[error("error building script: {0}")]
464    ScriptBuilderError(#[from] ScriptBuilderError),
465    #[error("transaction script template error: {0}")]
466    ScriptTemplateError(String),
467    #[error("storage slot {0} not found in account ID {1}")]
468    StorageSlotNotFound(u8, AccountId),
469    #[error("error while building the input notes: {0}")]
470    TransactionInputError(#[from] TransactionInputError),
471}
472
473// TESTS
474// ================================================================================================
475
476#[cfg(test)]
477mod tests {
478    use std::vec::Vec;
479
480    use miden_lib::account::auth::AuthRpoFalcon512;
481    use miden_lib::note::create_p2id_note;
482    use miden_lib::testing::account_component::MockAccountComponent;
483    use miden_objects::account::{AccountBuilder, AccountId, AccountType};
484    use miden_objects::asset::FungibleAsset;
485    use miden_objects::crypto::dsa::rpo_falcon512::PublicKey;
486    use miden_objects::crypto::rand::{FeltRng, RpoRandomCoin};
487    use miden_objects::note::{NoteTag, NoteType};
488    use miden_objects::testing::account_id::{
489        ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
490        ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
491        ACCOUNT_ID_SENDER,
492    };
493    use miden_objects::transaction::OutputNote;
494    use miden_objects::{EMPTY_WORD, Felt, Word, ZERO};
495    use miden_tx::utils::{Deserializable, Serializable};
496
497    use super::{TransactionRequest, TransactionRequestBuilder};
498    use crate::rpc::domain::account::AccountStorageRequirements;
499    use crate::transaction::ForeignAccount;
500
501    #[test]
502    fn transaction_request_serialization() {
503        let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
504        let target_id =
505            AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
506        let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
507        let mut rng = RpoRandomCoin::new(Word::default());
508
509        let mut notes = vec![];
510        for i in 0..6 {
511            let note = create_p2id_note(
512                sender_id,
513                target_id,
514                vec![FungibleAsset::new(faucet_id, 100 + i).unwrap().into()],
515                NoteType::Private,
516                ZERO,
517                &mut rng,
518            )
519            .unwrap();
520            notes.push(note);
521        }
522
523        let mut advice_vec: Vec<(Word, Vec<Felt>)> = vec![];
524        for i in 0..10 {
525            advice_vec.push((rng.draw_word(), vec![Felt::new(i)]));
526        }
527
528        let account = AccountBuilder::new(Default::default())
529            .with_component(MockAccountComponent::with_empty_slots())
530            .with_auth_component(AuthRpoFalcon512::new(PublicKey::new(EMPTY_WORD)))
531            .account_type(AccountType::RegularAccountImmutableCode)
532            .storage_mode(miden_objects::account::AccountStorageMode::Private)
533            .build_existing()
534            .unwrap();
535
536        // This transaction request wouldn't be valid in a real scenario, it's intended for testing
537        let tx_request = TransactionRequestBuilder::new()
538            .authenticated_input_notes(vec![(notes.pop().unwrap().id(), None)])
539            .unauthenticated_input_notes(vec![(notes.pop().unwrap(), None)])
540            .expected_output_recipients(vec![notes.pop().unwrap().recipient().clone()])
541            .expected_future_notes(vec![(
542                notes.pop().unwrap().into(),
543                NoteTag::from_account_id(sender_id),
544            )])
545            .extend_advice_map(advice_vec)
546            .foreign_accounts([
547                ForeignAccount::public(
548                    target_id,
549                    AccountStorageRequirements::new([(5u8, &[Word::default()])]),
550                )
551                .unwrap(),
552                ForeignAccount::private(account).unwrap(),
553            ])
554            .own_output_notes(vec![
555                OutputNote::Full(notes.pop().unwrap()),
556                OutputNote::Partial(notes.pop().unwrap().into()),
557            ])
558            .script_arg(rng.draw_word())
559            .auth_arg(rng.draw_word())
560            .build()
561            .unwrap();
562
563        let mut buffer = Vec::new();
564        tx_request.write_into(&mut buffer);
565
566        let deserialized_tx_request = TransactionRequest::read_from_bytes(&buffer).unwrap();
567        assert_eq!(tx_request, deserialized_tx_request);
568    }
569}