Skip to main content

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