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::{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, ForeignAccountInputs};
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.
80    expiration_delta: Option<u16>,
81}
82
83impl TransactionRequest {
84    // PUBLIC ACCESSORS
85    // --------------------------------------------------------------------------------------------
86
87    /// Returns a reference to the transaction request's unauthenticated note list.
88    pub fn unauthenticated_input_notes(&self) -> &[Note] {
89        &self.unauthenticated_input_notes
90    }
91
92    /// Returns an iterator over unauthenticated note IDs for the transaction request.
93    pub fn unauthenticated_input_note_ids(&self) -> impl Iterator<Item = NoteId> + '_ {
94        self.unauthenticated_input_notes.iter().map(Note::id)
95    }
96
97    /// Returns an iterator over authenticated input note IDs for the transaction request.
98    pub fn authenticated_input_note_ids(&self) -> impl Iterator<Item = NoteId> + '_ {
99        let unauthenticated_note_ids =
100            self.unauthenticated_input_note_ids().collect::<BTreeSet<_>>();
101
102        self.input_notes()
103            .iter()
104            .map(|(note_id, _)| *note_id)
105            .filter(move |note_id| !unauthenticated_note_ids.contains(note_id))
106    }
107
108    /// Returns a mapping for input note IDs and their optional [`NoteArgs`].
109    pub fn input_notes(&self) -> &BTreeMap<NoteId, Option<NoteArgs>> {
110        &self.input_notes
111    }
112
113    /// Returns a list of all input note IDs.
114    pub fn get_input_note_ids(&self) -> Vec<NoteId> {
115        self.input_notes.keys().copied().collect()
116    }
117
118    /// Returns a map of note IDs to their respective [`NoteArgs`]. The result will include
119    /// exclusively note IDs for notes for which [`NoteArgs`] have been defined.
120    pub fn get_note_args(&self) -> BTreeMap<NoteId, NoteArgs> {
121        self.input_notes
122            .iter()
123            .filter_map(|(note, args)| args.map(|a| (*note, a)))
124            .collect()
125    }
126
127    /// Returns an iterator over the expected output notes.
128    pub fn expected_output_notes(&self) -> impl Iterator<Item = &Note> {
129        self.expected_output_notes.values()
130    }
131
132    /// Returns an iterator over expected future notes.
133    pub fn expected_future_notes(&self) -> impl Iterator<Item = &(NoteDetails, NoteTag)> {
134        self.expected_future_notes.values()
135    }
136
137    /// Returns the [`TransactionScriptTemplate`].
138    pub fn script_template(&self) -> &Option<TransactionScriptTemplate> {
139        &self.script_template
140    }
141
142    /// Returns the [`AdviceMap`] for the transaction request.
143    pub fn advice_map(&self) -> &AdviceMap {
144        &self.advice_map
145    }
146
147    /// Returns the [`MerkleStore`] for the transaction request.
148    pub fn merkle_store(&self) -> &MerkleStore {
149        &self.merkle_store
150    }
151
152    /// Returns the IDs of the required foreign accounts for the transaction request.
153    pub fn foreign_accounts(&self) -> &BTreeSet<ForeignAccount> {
154        &self.foreign_accounts
155    }
156
157    /// Converts the [`TransactionRequest`] into [`TransactionArgs`] in order to be executed by a
158    /// Miden host.
159    pub(super) fn into_transaction_args(self, tx_script: TransactionScript) -> TransactionArgs {
160        let note_args = self.get_note_args();
161        let TransactionRequest {
162            expected_output_notes,
163            advice_map,
164            merkle_store,
165            ..
166        } = self;
167
168        let mut tx_args = TransactionArgs::new(Some(tx_script), note_args.into(), advice_map);
169
170        tx_args.extend_expected_output_notes(expected_output_notes.into_values());
171        tx_args.extend_merkle_store(merkle_store.inner_nodes());
172
173        tx_args
174    }
175
176    /// Builds the transaction script based on the account capabilities and the transaction request.
177    /// The debug mode enables the script debug logs.
178    pub(crate) fn build_transaction_script(
179        &self,
180        account_interface: &AccountInterface,
181        in_debug_mode: bool,
182    ) -> Result<TransactionScript, TransactionRequestError> {
183        match &self.script_template {
184            Some(TransactionScriptTemplate::CustomScript(script)) => Ok(script.clone()),
185            Some(TransactionScriptTemplate::SendNotes(notes)) => Ok(account_interface
186                .build_send_notes_script(notes, self.expiration_delta, in_debug_mode)?),
187            None => {
188                if self.input_notes.is_empty() {
189                    Err(TransactionRequestError::NoInputNotes)
190                } else {
191                    Ok(account_interface.build_auth_script(in_debug_mode)?)
192                }
193            },
194        }
195    }
196}
197
198// SERIALIZATION
199// ================================================================================================
200
201impl Serializable for TransactionRequest {
202    fn write_into<W: ByteWriter>(&self, target: &mut W) {
203        self.unauthenticated_input_notes.write_into(target);
204        self.input_notes.write_into(target);
205        match &self.script_template {
206            None => target.write_u8(0),
207            Some(TransactionScriptTemplate::CustomScript(script)) => {
208                target.write_u8(1);
209                script.write_into(target);
210            },
211            Some(TransactionScriptTemplate::SendNotes(notes)) => {
212                target.write_u8(2);
213                notes.write_into(target);
214            },
215        }
216        self.expected_output_notes.write_into(target);
217        self.expected_future_notes.write_into(target);
218        self.advice_map.clone().into_iter().collect::<Vec<_>>().write_into(target);
219        self.merkle_store.write_into(target);
220        self.foreign_accounts.write_into(target);
221        self.expiration_delta.write_into(target);
222    }
223}
224
225impl Deserializable for TransactionRequest {
226    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
227        let unauthenticated_input_notes = Vec::<Note>::read_from(source)?;
228        let input_notes = BTreeMap::<NoteId, Option<NoteArgs>>::read_from(source)?;
229
230        let script_template = match source.read_u8()? {
231            0 => None,
232            1 => {
233                let transaction_script = TransactionScript::read_from(source)?;
234                Some(TransactionScriptTemplate::CustomScript(transaction_script))
235            },
236            2 => {
237                let notes = Vec::<PartialNote>::read_from(source)?;
238                Some(TransactionScriptTemplate::SendNotes(notes))
239            },
240            _ => {
241                return Err(DeserializationError::InvalidValue(
242                    "Invalid script template type".to_string(),
243                ));
244            },
245        };
246
247        let expected_output_notes = BTreeMap::<NoteId, Note>::read_from(source)?;
248        let expected_future_notes = BTreeMap::<NoteId, (NoteDetails, NoteTag)>::read_from(source)?;
249
250        let mut advice_map = AdviceMap::new();
251        let advice_vec = Vec::<(Digest, Vec<Felt>)>::read_from(source)?;
252        advice_map.extend(advice_vec);
253        let merkle_store = MerkleStore::read_from(source)?;
254        let foreign_accounts = BTreeSet::<ForeignAccount>::read_from(source)?;
255        let expiration_delta = Option::<u16>::read_from(source)?;
256
257        Ok(TransactionRequest {
258            unauthenticated_input_notes,
259            input_notes,
260            script_template,
261            expected_output_notes,
262            expected_future_notes,
263            advice_map,
264            merkle_store,
265            foreign_accounts,
266            expiration_delta,
267        })
268    }
269}
270
271impl Default for TransactionRequestBuilder {
272    fn default() -> Self {
273        Self::new()
274    }
275}
276
277// TRANSACTION REQUEST ERROR
278// ================================================================================================
279
280// Errors related to a [TransactionRequest]
281#[derive(Debug, Error)]
282pub enum TransactionRequestError {
283    #[error("foreign account data missing in the account proof")]
284    ForeignAccountDataMissing,
285    #[error("foreign account storage slot {0} is not a map type")]
286    ForeignAccountStorageSlotInvalidIndex(u8),
287    #[error("requested foreign account with ID {0} does not have an expected storage mode")]
288    InvalidForeignAccountId(AccountId),
289    #[error(
290        "every authenticated note to be consumed should be committed and contain a valid inclusion proof"
291    )]
292    InputNoteNotAuthenticated,
293    #[error("the input notes map should include keys for all provided unauthenticated input notes")]
294    InputNotesMapMissingUnauthenticatedNotes,
295    #[error("own notes shouldn't be of the header variant")]
296    InvalidNoteVariant,
297    #[error("invalid sender account id: {0}")]
298    InvalidSenderAccount(AccountId),
299    #[error("invalid transaction script")]
300    InvalidTransactionScript(#[from] AssemblyError),
301    #[error("a transaction without output notes must have at least one input note")]
302    NoInputNotes,
303    #[error("note not found: {0}")]
304    NoteNotFound(String),
305    #[error("note creation error")]
306    NoteCreationError(#[from] NoteError),
307    #[error("pay to id note doesn't contain at least one asset")]
308    P2IDNoteWithoutAsset,
309    #[error("transaction script template error: {0}")]
310    ScriptTemplateError(String),
311    #[error("storage slot {0} not found in account ID {1}")]
312    StorageSlotNotFound(u8, AccountId),
313    #[error("account interface error")]
314    AccountInterfaceError(#[from] AccountInterfaceError),
315}
316
317// TESTS
318// ================================================================================================
319
320#[cfg(test)]
321mod tests {
322    use std::vec::Vec;
323
324    use miden_lib::{note::create_p2id_note, transaction::TransactionKernel};
325    use miden_objects::{
326        Digest, Felt, ZERO,
327        account::{AccountBuilder, AccountId, AccountIdAnchor, AccountType},
328        asset::FungibleAsset,
329        crypto::rand::{FeltRng, RpoRandomCoin},
330        note::{NoteExecutionMode, NoteTag, NoteType},
331        testing::{
332            account_component::AccountMockComponent,
333            account_id::{
334                ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
335                ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, ACCOUNT_ID_SENDER,
336            },
337        },
338        transaction::OutputNote,
339    };
340    use miden_tx::utils::{Deserializable, Serializable};
341
342    use super::{TransactionRequest, TransactionRequestBuilder};
343    use crate::{
344        rpc::domain::account::AccountStorageRequirements,
345        transaction::{ForeignAccount, ForeignAccountInputs},
346    };
347
348    #[test]
349    fn transaction_request_serialization() {
350        let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
351        let target_id =
352            AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
353        let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
354        let mut rng = RpoRandomCoin::new(Default::default());
355
356        let mut notes = vec![];
357        for i in 0..6 {
358            let note = create_p2id_note(
359                sender_id,
360                target_id,
361                vec![FungibleAsset::new(faucet_id, 100 + i).unwrap().into()],
362                NoteType::Private,
363                ZERO,
364                &mut rng,
365            )
366            .unwrap();
367            notes.push(note);
368        }
369
370        let mut advice_vec: Vec<(Digest, Vec<Felt>)> = vec![];
371        for i in 0..10 {
372            advice_vec.push((Digest::new(rng.draw_word()), vec![Felt::new(i)]));
373        }
374
375        let account = AccountBuilder::new(Default::default())
376            .anchor(AccountIdAnchor::new_unchecked(0, Digest::default()))
377            .with_component(
378                AccountMockComponent::new_with_empty_slots(TransactionKernel::assembler()).unwrap(),
379            )
380            .account_type(AccountType::RegularAccountImmutableCode)
381            .storage_mode(miden_objects::account::AccountStorageMode::Private)
382            .build_existing()
383            .unwrap();
384
385        // This transaction request wouldn't be valid in a real scenario, it's intended for testing
386        let tx_request = TransactionRequestBuilder::new()
387            .with_authenticated_input_notes(vec![(notes.pop().unwrap().id(), None)])
388            .with_unauthenticated_input_notes(vec![(notes.pop().unwrap(), None)])
389            .with_expected_output_notes(vec![notes.pop().unwrap()])
390            .with_expected_future_notes(vec![(
391                notes.pop().unwrap().into(),
392                NoteTag::from_account_id(sender_id, NoteExecutionMode::Local).unwrap(),
393            )])
394            .extend_advice_map(advice_vec)
395            .with_foreign_accounts([
396                ForeignAccount::public(
397                    target_id,
398                    AccountStorageRequirements::new([(5u8, &[Digest::default()])]),
399                )
400                .unwrap(),
401                ForeignAccount::private(
402                    ForeignAccountInputs::from_account(
403                        account,
404                        &AccountStorageRequirements::default(),
405                    )
406                    .unwrap(),
407                )
408                .unwrap(),
409            ])
410            .with_own_output_notes(vec![
411                OutputNote::Full(notes.pop().unwrap()),
412                OutputNote::Partial(notes.pop().unwrap().into()),
413            ])
414            .build()
415            .unwrap();
416
417        let mut buffer = Vec::new();
418        tx_request.write_into(&mut buffer);
419
420        let deserialized_tx_request = TransactionRequest::read_from_bytes(&buffer).unwrap();
421        assert_eq!(tx_request, deserialized_tx_request);
422    }
423}