miden_client/transaction/request/
mod.rs

1//! Contains structures and functions related to transaction creation.
2
3use alloc::{
4    collections::{BTreeMap, BTreeSet},
5    string::{String, ToString},
6    vec::Vec,
7};
8
9use miden_lib::account::interface::{AccountInterface, AccountInterfaceError};
10use miden_objects::{
11    Digest, Felt, NoteError, Word,
12    account::AccountId,
13    assembly::AssemblyError,
14    crypto::merkle::MerkleStore,
15    note::{Note, NoteDetails, NoteId, NoteTag, PartialNote},
16    transaction::{AccountInputs, TransactionArgs, TransactionScript},
17    vm::AdviceMap,
18};
19use miden_tx::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable};
20use thiserror::Error;
21
22mod builder;
23pub use builder::{PaymentTransactionData, SwapTransactionData, TransactionRequestBuilder};
24
25mod foreign;
26pub use foreign::ForeignAccount;
27
28// TRANSACTION REQUEST
29// ================================================================================================
30
31pub type NoteArgs = Word;
32
33/// Specifies a transaction script to be executed in a transaction.
34///
35/// A transaction script is a program which is executed after scripts of all input notes have been
36/// executed.
37#[derive(Clone, Debug, PartialEq, Eq)]
38pub enum TransactionScriptTemplate {
39    /// Specifies the exact transaction script to be executed in a transaction.
40    CustomScript(TransactionScript),
41    /// Specifies that the transaction script must create the specified output notes.
42    ///
43    /// It is up to the client to determine how the output notes will be created and this will
44    /// depend on the capabilities of the account the transaction request will be applied to.
45    /// For example, for Basic Wallets, this may involve invoking `create_note` procedure.
46    SendNotes(Vec<PartialNote>),
47}
48
49/// Specifies a transaction request that can be executed by an account.
50///
51/// A request contains information about input notes to be consumed by the transaction (if any),
52/// description of the transaction script to be executed (if any), and a set of notes expected
53/// to be generated by the transaction or by consuming notes generated by the transaction.
54#[derive(Clone, Debug, PartialEq, Eq)]
55pub struct TransactionRequest {
56    /// Notes to be consumed by the transaction that aren't authenticated.
57    unauthenticated_input_notes: Vec<Note>,
58    /// Notes to be consumed by the transaction together with their (optional) arguments. This
59    /// includes both authenticated and unauthenticated notes.
60    input_notes: BTreeMap<NoteId, Option<NoteArgs>>,
61    /// Template for the creation of the transaction script.
62    script_template: Option<TransactionScriptTemplate>,
63    /// A map of notes expected to be generated by the transactions.
64    expected_output_notes: BTreeMap<NoteId, Note>,
65    /// A map of details and tags of notes we expect to be created as part of future transactions
66    /// with their respective tags.
67    ///
68    /// For example, after a swap note is consumed, a payback note is expected to be created.
69    expected_future_notes: BTreeMap<NoteId, (NoteDetails, NoteTag)>,
70    /// Initial state of the `AdviceMap` that provides data during runtime.
71    advice_map: AdviceMap,
72    /// Initial state of the `MerkleStore` that provides data during runtime.
73    merkle_store: MerkleStore,
74    /// Foreign account data requirements. At execution time, account data will be retrieved from
75    /// the network, and injected as advice inputs. Additionally, the account's code will be
76    /// added to the executor and prover.
77    foreign_accounts: BTreeSet<ForeignAccount>,
78    /// The number of blocks in relation to the transaction's reference block after which the
79    /// transaction will expire. If `None`, the transaction will not expire.
80    expiration_delta: Option<u16>,
81    /// Indicates whether to **silently** ignore invalid input notes when executing the
82    /// transaction. This will allow the transaction to be executed even if some input notes
83    /// are invalid.
84    ignore_invalid_input_notes: bool,
85}
86
87impl TransactionRequest {
88    // PUBLIC ACCESSORS
89    // --------------------------------------------------------------------------------------------
90
91    /// Returns a reference to the transaction request's unauthenticated note list.
92    pub fn unauthenticated_input_notes(&self) -> &[Note] {
93        &self.unauthenticated_input_notes
94    }
95
96    /// Returns an iterator over unauthenticated note IDs for the transaction request.
97    pub fn unauthenticated_input_note_ids(&self) -> impl Iterator<Item = NoteId> + '_ {
98        self.unauthenticated_input_notes.iter().map(Note::id)
99    }
100
101    /// Returns an iterator over authenticated input note IDs for the transaction request.
102    pub fn authenticated_input_note_ids(&self) -> impl Iterator<Item = NoteId> + '_ {
103        let unauthenticated_note_ids =
104            self.unauthenticated_input_note_ids().collect::<BTreeSet<_>>();
105
106        self.input_notes()
107            .keys()
108            .copied()
109            .filter(move |note_id| !unauthenticated_note_ids.contains(note_id))
110    }
111
112    /// Returns a mapping for input note IDs and their optional [`NoteArgs`].
113    pub fn input_notes(&self) -> &BTreeMap<NoteId, Option<NoteArgs>> {
114        &self.input_notes
115    }
116
117    /// Returns a list of all input note IDs.
118    pub fn get_input_note_ids(&self) -> Vec<NoteId> {
119        self.input_notes.keys().copied().collect()
120    }
121
122    /// Returns a map of note IDs to their respective [`NoteArgs`]. The result will include
123    /// exclusively note IDs for notes for which [`NoteArgs`] have been defined.
124    pub fn get_note_args(&self) -> BTreeMap<NoteId, NoteArgs> {
125        self.input_notes
126            .iter()
127            .filter_map(|(note, args)| args.map(|a| (*note, a)))
128            .collect()
129    }
130
131    /// Returns an iterator over the expected output notes.
132    pub fn expected_output_notes(&self) -> impl Iterator<Item = &Note> {
133        self.expected_output_notes.values()
134    }
135
136    /// Returns an iterator over expected future notes.
137    pub fn expected_future_notes(&self) -> impl Iterator<Item = &(NoteDetails, NoteTag)> {
138        self.expected_future_notes.values()
139    }
140
141    /// Returns the [`TransactionScriptTemplate`].
142    pub fn script_template(&self) -> &Option<TransactionScriptTemplate> {
143        &self.script_template
144    }
145
146    /// Returns the [`AdviceMap`] for the transaction request.
147    pub fn advice_map(&self) -> &AdviceMap {
148        &self.advice_map
149    }
150
151    /// Returns the [`MerkleStore`] for the transaction request.
152    pub fn merkle_store(&self) -> &MerkleStore {
153        &self.merkle_store
154    }
155
156    /// Returns the IDs of the required foreign accounts for the transaction request.
157    pub fn foreign_accounts(&self) -> &BTreeSet<ForeignAccount> {
158        &self.foreign_accounts
159    }
160
161    /// Returns whether to ignore invalid input notes or not.
162    pub fn ignore_invalid_input_notes(&self) -> bool {
163        self.ignore_invalid_input_notes
164    }
165
166    /// Converts the [`TransactionRequest`] into [`TransactionArgs`] in order to be executed by a
167    /// Miden host.
168    pub(crate) fn into_transaction_args(
169        self,
170        tx_script: TransactionScript,
171        foreign_account_inputs: Vec<AccountInputs>,
172    ) -> TransactionArgs {
173        let note_args = self.get_note_args();
174        let TransactionRequest {
175            expected_output_notes,
176            advice_map,
177            merkle_store,
178            ..
179        } = self;
180
181        let mut tx_args = TransactionArgs::new(
182            Some(tx_script),
183            note_args.into(),
184            advice_map,
185            foreign_account_inputs,
186        );
187
188        tx_args.extend_output_note_recipients(expected_output_notes.into_values());
189        tx_args.extend_merkle_store(merkle_store.inner_nodes());
190
191        tx_args
192    }
193
194    /// Builds the transaction script based on the account capabilities and the transaction request.
195    /// The debug mode enables the script debug logs.
196    pub(crate) fn build_transaction_script(
197        &self,
198        account_interface: &AccountInterface,
199        in_debug_mode: bool,
200    ) -> Result<TransactionScript, TransactionRequestError> {
201        match &self.script_template {
202            Some(TransactionScriptTemplate::CustomScript(script)) => Ok(script.clone()),
203            Some(TransactionScriptTemplate::SendNotes(notes)) => Ok(account_interface
204                .build_send_notes_script(notes, self.expiration_delta, in_debug_mode)?),
205            None => {
206                if self.input_notes.is_empty() {
207                    Err(TransactionRequestError::NoInputNotes)
208                } else {
209                    Ok(account_interface.build_auth_script(in_debug_mode)?)
210                }
211            },
212        }
213    }
214}
215
216// SERIALIZATION
217// ================================================================================================
218
219impl Serializable for TransactionRequest {
220    fn write_into<W: ByteWriter>(&self, target: &mut W) {
221        self.unauthenticated_input_notes.write_into(target);
222        self.input_notes.write_into(target);
223        match &self.script_template {
224            None => target.write_u8(0),
225            Some(TransactionScriptTemplate::CustomScript(script)) => {
226                target.write_u8(1);
227                script.write_into(target);
228            },
229            Some(TransactionScriptTemplate::SendNotes(notes)) => {
230                target.write_u8(2);
231                notes.write_into(target);
232            },
233        }
234        self.expected_output_notes.write_into(target);
235        self.expected_future_notes.write_into(target);
236        self.advice_map.clone().into_iter().collect::<Vec<_>>().write_into(target);
237        self.merkle_store.write_into(target);
238        self.foreign_accounts.write_into(target);
239        self.expiration_delta.write_into(target);
240        target.write_u8(u8::from(self.ignore_invalid_input_notes));
241    }
242}
243
244impl Deserializable for TransactionRequest {
245    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
246        let unauthenticated_input_notes = Vec::<Note>::read_from(source)?;
247        let input_notes = BTreeMap::<NoteId, Option<NoteArgs>>::read_from(source)?;
248
249        let script_template = match source.read_u8()? {
250            0 => None,
251            1 => {
252                let transaction_script = TransactionScript::read_from(source)?;
253                Some(TransactionScriptTemplate::CustomScript(transaction_script))
254            },
255            2 => {
256                let notes = Vec::<PartialNote>::read_from(source)?;
257                Some(TransactionScriptTemplate::SendNotes(notes))
258            },
259            _ => {
260                return Err(DeserializationError::InvalidValue(
261                    "Invalid script template type".to_string(),
262                ));
263            },
264        };
265
266        let expected_output_notes = BTreeMap::<NoteId, Note>::read_from(source)?;
267        let expected_future_notes = BTreeMap::<NoteId, (NoteDetails, NoteTag)>::read_from(source)?;
268
269        let mut advice_map = AdviceMap::new();
270        let advice_vec = Vec::<(Digest, Vec<Felt>)>::read_from(source)?;
271        advice_map.extend(advice_vec);
272        let merkle_store = MerkleStore::read_from(source)?;
273        let foreign_accounts = BTreeSet::<ForeignAccount>::read_from(source)?;
274        let expiration_delta = Option::<u16>::read_from(source)?;
275        let ignore_invalid_input_notes = source.read_u8()? == 1;
276
277        Ok(TransactionRequest {
278            unauthenticated_input_notes,
279            input_notes,
280            script_template,
281            expected_output_notes,
282            expected_future_notes,
283            advice_map,
284            merkle_store,
285            foreign_accounts,
286            expiration_delta,
287            ignore_invalid_input_notes,
288        })
289    }
290}
291
292impl Default for TransactionRequestBuilder {
293    fn default() -> Self {
294        Self::new()
295    }
296}
297
298// TRANSACTION REQUEST ERROR
299// ================================================================================================
300
301// Errors related to a [TransactionRequest]
302#[derive(Debug, Error)]
303pub enum TransactionRequestError {
304    #[error("foreign account data missing in the account proof")]
305    ForeignAccountDataMissing,
306    #[error("foreign account storage slot {0} is not a map type")]
307    ForeignAccountStorageSlotInvalidIndex(u8),
308    #[error("requested foreign account with ID {0} does not have an expected storage mode")]
309    InvalidForeignAccountId(AccountId),
310    #[error("note {0} does not contain a valid inclusion proof")]
311    InputNoteNotAuthenticated(NoteId),
312    #[error("note {0} has already been consumed")]
313    InputNoteAlreadyConsumed(NoteId),
314    #[error("the input notes map should include keys for all provided unauthenticated input notes")]
315    InputNotesMapMissingUnauthenticatedNotes,
316    #[error("own notes shouldn't be of the header variant")]
317    InvalidNoteVariant,
318    #[error("invalid sender account id: {0}")]
319    InvalidSenderAccount(AccountId),
320    #[error("invalid transaction script")]
321    InvalidTransactionScript(#[from] AssemblyError),
322    #[error("a transaction without output notes must have at least one input note")]
323    NoInputNotes,
324    #[error("note not found: {0}")]
325    NoteNotFound(String),
326    #[error("note creation error")]
327    NoteCreationError(#[from] NoteError),
328    #[error("pay to id note doesn't contain at least one asset")]
329    P2IDNoteWithoutAsset,
330    #[error("transaction script template error: {0}")]
331    ScriptTemplateError(String),
332    #[error("storage slot {0} not found in account ID {1}")]
333    StorageSlotNotFound(u8, AccountId),
334    #[error("account interface error")]
335    AccountInterfaceError(#[from] AccountInterfaceError),
336}
337
338// TESTS
339// ================================================================================================
340
341#[cfg(test)]
342mod tests {
343    use std::vec::Vec;
344
345    use miden_lib::{note::create_p2id_note, transaction::TransactionKernel};
346    use miden_objects::{
347        Digest, Felt, ZERO,
348        account::{AccountBuilder, AccountId, AccountIdAnchor, AccountType},
349        asset::FungibleAsset,
350        crypto::rand::{FeltRng, RpoRandomCoin},
351        note::{NoteExecutionMode, NoteTag, NoteType},
352        testing::{
353            account_component::AccountMockComponent,
354            account_id::{
355                ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
356                ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, ACCOUNT_ID_SENDER,
357            },
358        },
359        transaction::OutputNote,
360    };
361    use miden_tx::utils::{Deserializable, Serializable};
362
363    use super::{TransactionRequest, TransactionRequestBuilder};
364    use crate::{rpc::domain::account::AccountStorageRequirements, transaction::ForeignAccount};
365
366    #[test]
367    fn transaction_request_serialization() {
368        let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
369        let target_id =
370            AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
371        let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
372        let mut rng = RpoRandomCoin::new(Default::default());
373
374        let mut notes = vec![];
375        for i in 0..6 {
376            let note = create_p2id_note(
377                sender_id,
378                target_id,
379                vec![FungibleAsset::new(faucet_id, 100 + i).unwrap().into()],
380                NoteType::Private,
381                ZERO,
382                &mut rng,
383            )
384            .unwrap();
385            notes.push(note);
386        }
387
388        let mut advice_vec: Vec<(Digest, Vec<Felt>)> = vec![];
389        for i in 0..10 {
390            advice_vec.push((Digest::new(rng.draw_word()), vec![Felt::new(i)]));
391        }
392
393        let account = AccountBuilder::new(Default::default())
394            .anchor(AccountIdAnchor::new_unchecked(0, Digest::default()))
395            .with_component(
396                AccountMockComponent::new_with_empty_slots(TransactionKernel::assembler()).unwrap(),
397            )
398            .account_type(AccountType::RegularAccountImmutableCode)
399            .storage_mode(miden_objects::account::AccountStorageMode::Private)
400            .build_existing()
401            .unwrap();
402
403        // This transaction request wouldn't be valid in a real scenario, it's intended for testing
404        let tx_request = TransactionRequestBuilder::new()
405            .with_authenticated_input_notes(vec![(notes.pop().unwrap().id(), None)])
406            .with_unauthenticated_input_notes(vec![(notes.pop().unwrap(), None)])
407            .with_expected_output_notes(vec![notes.pop().unwrap()])
408            .with_expected_future_notes(vec![(
409                notes.pop().unwrap().into(),
410                NoteTag::from_account_id(sender_id, NoteExecutionMode::Local).unwrap(),
411            )])
412            .extend_advice_map(advice_vec)
413            .with_foreign_accounts([
414                ForeignAccount::public(
415                    target_id,
416                    AccountStorageRequirements::new([(5u8, &[Digest::default()])]),
417                )
418                .unwrap(),
419                ForeignAccount::private(account).unwrap(),
420            ])
421            .with_own_output_notes(vec![
422                OutputNote::Full(notes.pop().unwrap()),
423                OutputNote::Partial(notes.pop().unwrap().into()),
424            ])
425            .build()
426            .unwrap();
427
428        let mut buffer = Vec::new();
429        tx_request.write_into(&mut buffer);
430
431        let deserialized_tx_request = TransactionRequest::read_from_bytes(&buffer).unwrap();
432        assert_eq!(tx_request, deserialized_tx_request);
433    }
434}