miden_client/transaction/request/
mod.rs

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