miden_client/transaction/request/
builder.rs

1//! Contains structures and functions related to transaction creation.
2use alloc::collections::{BTreeMap, BTreeSet};
3use alloc::string::ToString;
4use alloc::vec::Vec;
5
6use miden_lib::note::{create_p2id_note, create_p2ide_note, create_swap_note};
7use miden_objects::account::AccountId;
8use miden_objects::asset::{Asset, FungibleAsset};
9use miden_objects::block::BlockNumber;
10use miden_objects::crypto::merkle::{InnerNodeInfo, MerkleStore};
11use miden_objects::note::{
12    Note,
13    NoteDetails,
14    NoteId,
15    NoteRecipient,
16    NoteTag,
17    NoteType,
18    PartialNote,
19};
20use miden_objects::transaction::{OutputNote, TransactionScript};
21use miden_objects::vm::AdviceMap;
22use miden_objects::{Felt, FieldElement, NoteError, Word};
23
24use super::{
25    ForeignAccount,
26    NoteArgs,
27    TransactionRequest,
28    TransactionRequestError,
29    TransactionScriptTemplate,
30};
31use crate::ClientRng;
32
33// TRANSACTION REQUEST BUILDER
34// ================================================================================================
35
36/// A builder for a [`TransactionRequest`].
37///
38/// Use this builder to construct a [`TransactionRequest`] by adding input notes, specifying
39/// scripts, and setting other transaction parameters.
40#[derive(Clone, Debug)]
41pub struct TransactionRequestBuilder {
42    /// Notes to be consumed by the transaction that aren't authenticated.
43    unauthenticated_input_notes: Vec<Note>,
44    /// Notes to be consumed by the transaction together with their (optional) arguments. This
45    /// includes both authenticated and unauthenticated notes.
46    input_notes: Vec<(NoteId, Option<NoteArgs>)>,
47    /// Notes to be created by the transaction. This includes both full and partial output notes.
48    /// The transaction script will be generated based on these notes.
49    own_output_notes: Vec<OutputNote>,
50    /// A map of recipients of the output notes expected to be generated by the transaction.
51    expected_output_recipients: BTreeMap<Word, NoteRecipient>,
52    /// A map of details and tags of notes we expect to be created as part of future transactions
53    /// with their respective tags.
54    ///
55    /// For example, after a swap note is consumed, a payback note is expected to be created.
56    expected_future_notes: BTreeMap<NoteId, (NoteDetails, NoteTag)>,
57    /// Custom transaction script to be used.
58    custom_script: Option<TransactionScript>,
59    /// Initial state of the `AdviceMap` that provides data during runtime.
60    advice_map: AdviceMap,
61    /// Initial state of the `MerkleStore` that provides data during runtime.
62    merkle_store: MerkleStore,
63    /// Foreign account data requirements. At execution time, account data will be retrieved from
64    /// the network, and injected as advice inputs. Additionally, the account's code will be
65    /// added to the executor and prover.
66    foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
67    /// The number of blocks in relation to the transaction's reference block after which the
68    /// transaction will expire. If `None`, the transaction will not expire.
69    expiration_delta: Option<u16>,
70    /// Indicates whether to **silently** ignore invalid input notes when executing the
71    /// transaction. This will allow the transaction to be executed even if some input notes
72    /// are invalid.
73    ignore_invalid_input_notes: bool,
74    /// Optional [`Word`] that will be pushed to the operand stack before the transaction script
75    /// execution. If the advice map is extended with some user defined entries, this script
76    /// argument could be used as a key to access the corresponding value.
77    script_arg: Option<Word>,
78    /// Optional [`Word`] that will be pushed to the stack for the authentication procedure
79    /// during transaction execution.
80    auth_arg: Option<Word>,
81}
82
83impl TransactionRequestBuilder {
84    // CONSTRUCTORS
85    // --------------------------------------------------------------------------------------------
86
87    /// Creates a new, empty [`TransactionRequestBuilder`].
88    pub fn new() -> Self {
89        Self {
90            unauthenticated_input_notes: vec![],
91            input_notes: vec![],
92            own_output_notes: Vec::new(),
93            expected_output_recipients: BTreeMap::new(),
94            expected_future_notes: BTreeMap::new(),
95            custom_script: None,
96            advice_map: AdviceMap::default(),
97            merkle_store: MerkleStore::default(),
98            expiration_delta: None,
99            foreign_accounts: BTreeMap::default(),
100            ignore_invalid_input_notes: false,
101            script_arg: None,
102            auth_arg: None,
103        }
104    }
105
106    /// Adds the specified notes as unauthenticated input notes to the transaction request.
107    #[must_use]
108    pub fn unauthenticated_input_notes(
109        mut self,
110        notes: impl IntoIterator<Item = (Note, Option<NoteArgs>)>,
111    ) -> Self {
112        for (note, argument) in notes {
113            self.input_notes.push((note.id(), argument));
114            self.unauthenticated_input_notes.push(note);
115        }
116        self
117    }
118
119    /// Adds the specified notes as authenticated input notes to the transaction request.
120    #[must_use]
121    pub fn authenticated_input_notes(
122        mut self,
123        notes: impl IntoIterator<Item = (NoteId, Option<NoteArgs>)>,
124    ) -> Self {
125        for (note_id, argument) in notes {
126            self.input_notes.push((note_id, argument));
127        }
128        self
129    }
130
131    /// Specifies the output notes that should be created in the transaction script and will
132    /// be used as a transaction script template. These notes will also be added to the expected
133    /// output recipients of the transaction.
134    ///
135    /// If a transaction script template is already set (e.g. by calling `with_custom_script`), the
136    /// [`TransactionRequestBuilder::build`] method will return an error.
137    #[must_use]
138    pub fn own_output_notes(mut self, notes: impl IntoIterator<Item = OutputNote>) -> Self {
139        for note in notes {
140            if let OutputNote::Full(note) = &note {
141                self.expected_output_recipients
142                    .insert(note.recipient().digest(), note.recipient().clone());
143            }
144
145            self.own_output_notes.push(note);
146        }
147
148        self
149    }
150
151    /// Specifies a custom transaction script to be used.
152    ///
153    /// If a script template is already set (e.g. by calling `with_own_output_notes`), the
154    /// [`TransactionRequestBuilder::build`] method will return an error.
155    #[must_use]
156    pub fn custom_script(mut self, script: TransactionScript) -> Self {
157        self.custom_script = Some(script);
158        self
159    }
160
161    /// Specifies one or more foreign accounts (public or private) that contain data
162    /// utilized by the transaction.
163    ///
164    /// At execution, the client queries the node and retrieves the appropriate data,
165    /// depending on whether each foreign account is public or private:
166    ///
167    /// - **Public accounts**: the node retrieves the state and code for the account and injects
168    ///   them as advice inputs.
169    /// - **Private accounts**: the node retrieves a proof of the account's existence and injects
170    ///   that as advice inputs.
171    #[must_use]
172    pub fn foreign_accounts(
173        mut self,
174        foreign_accounts: impl IntoIterator<Item = impl Into<ForeignAccount>>,
175    ) -> Self {
176        for account in foreign_accounts {
177            let foreign_account: ForeignAccount = account.into();
178            self.foreign_accounts.insert(foreign_account.account_id(), foreign_account);
179        }
180
181        self
182    }
183
184    /// Specifies a transaction's expected output note recipients.
185    ///
186    /// The set of specified recipients is treated as a subset of the recipients for notes that may
187    /// be created by a transaction. That is, the transaction must create notes for all the
188    /// specified expected recipients, but it may also create notes for other recipients not
189    /// included in this set.
190    #[must_use]
191    pub fn expected_output_recipients(mut self, recipients: Vec<NoteRecipient>) -> Self {
192        self.expected_output_recipients = recipients
193            .into_iter()
194            .map(|recipient| (recipient.digest(), recipient))
195            .collect::<BTreeMap<_, _>>();
196        self
197    }
198
199    /// Specifies a set of notes which may be created when a transaction's output notes are
200    /// consumed.
201    ///
202    /// For example, after a SWAP note is consumed, a payback note is expected to be created. This
203    /// allows the client to track this note accordingly.
204    #[must_use]
205    pub fn expected_future_notes(mut self, notes: Vec<(NoteDetails, NoteTag)>) -> Self {
206        self.expected_future_notes =
207            notes.into_iter().map(|note| (note.0.id(), note)).collect::<BTreeMap<_, _>>();
208        self
209    }
210
211    /// Extends the advice map with the specified `([Word], Vec<[Felt]>)` pairs.
212    #[must_use]
213    pub fn extend_advice_map<I, V>(mut self, iter: I) -> Self
214    where
215        I: IntoIterator<Item = (Word, V)>,
216        V: AsRef<[Felt]>,
217    {
218        self.advice_map.extend(iter.into_iter().map(|(w, v)| (w, v.as_ref().to_vec())));
219        self
220    }
221
222    /// Extends the merkle store with the specified [`InnerNodeInfo`] elements.
223    #[must_use]
224    pub fn extend_merkle_store<T: IntoIterator<Item = InnerNodeInfo>>(mut self, iter: T) -> Self {
225        self.merkle_store.extend(iter);
226        self
227    }
228
229    /// The number of blocks in relation to the transaction's reference block after which the
230    /// transaction will expire. By default, the transaction will not expire.
231    ///
232    /// Setting transaction expiration delta defines an upper bound for transaction expiration,
233    /// but other code executed during the transaction may impose an even smaller transaction
234    /// expiration delta.
235    #[must_use]
236    pub fn expiration_delta(mut self, expiration_delta: u16) -> Self {
237        self.expiration_delta = Some(expiration_delta);
238        self
239    }
240
241    /// The resulting transaction will **silently** ignore invalid input notes when being executed.
242    /// By default, this will not happen.
243    #[must_use]
244    pub fn ignore_invalid_input_notes(mut self) -> Self {
245        self.ignore_invalid_input_notes = true;
246        self
247    }
248
249    /// Sets an optional [`Word`] that will be pushed to the operand stack before the transaction
250    /// script execution. If the advice map is extended with some user defined entries, this script
251    /// argument could be used as a key to access the corresponding value.
252    #[must_use]
253    pub fn script_arg(mut self, script_arg: Word) -> Self {
254        self.script_arg = Some(script_arg);
255        self
256    }
257
258    /// Sets an optional [`Word`] that will be pushed to the stack for the authentication
259    /// procedure during transaction execution.
260    #[must_use]
261    pub fn auth_arg(mut self, auth_arg: Word) -> Self {
262        self.auth_arg = Some(auth_arg);
263        self
264    }
265
266    // STANDARDIZED REQUESTS
267    // --------------------------------------------------------------------------------------------
268
269    /// Consumes the builder and returns a [`TransactionRequest`] for a transaction to consume the
270    /// specified notes.
271    ///
272    /// - `note_ids` is a list of note IDs to be consumed.
273    pub fn build_consume_notes(
274        self,
275        note_ids: Vec<NoteId>,
276    ) -> Result<TransactionRequest, TransactionRequestError> {
277        let input_notes = note_ids.into_iter().map(|id| (id, None));
278        self.authenticated_input_notes(input_notes).build()
279    }
280
281    /// Consumes the builder and returns a [`TransactionRequest`] for a transaction to mint fungible
282    /// assets. This request must be executed against a fungible faucet account.
283    ///
284    /// - `asset` is the fungible asset to be minted.
285    /// - `target_id` is the account ID of the account to receive the minted asset.
286    /// - `note_type` determines the visibility of the note to be created.
287    /// - `rng` is the random number generator used to generate the serial number for the created
288    ///   note.
289    ///
290    /// This function cannot be used with a previously set custom script.
291    pub fn build_mint_fungible_asset(
292        self,
293        asset: FungibleAsset,
294        target_id: AccountId,
295        note_type: NoteType,
296        rng: &mut ClientRng,
297    ) -> Result<TransactionRequest, TransactionRequestError> {
298        let created_note = create_p2id_note(
299            asset.faucet_id(),
300            target_id,
301            vec![asset.into()],
302            note_type,
303            Felt::ZERO,
304            rng,
305        )?;
306
307        self.own_output_notes(vec![OutputNote::Full(created_note)]).build()
308    }
309
310    /// Consumes the builder and returns a [`TransactionRequest`] for a transaction to send a P2ID
311    /// or P2IDE note. This request must be executed against the wallet sender account.
312    ///
313    /// - `payment_data` is the data for the payment transaction that contains the asset to be
314    ///   transferred, the sender account ID, and the target account ID. If the recall or timelock
315    ///   heights are set, a P2IDE note will be created; otherwise, a P2ID note will be created.
316    /// - `note_type` determines the visibility of the note to be created.
317    /// - `rng` is the random number generator used to generate the serial number for the created
318    ///   note.
319    ///
320    /// This function cannot be used with a previously set custom script.
321    pub fn build_pay_to_id(
322        self,
323        payment_data: PaymentNoteDescription,
324        note_type: NoteType,
325        rng: &mut ClientRng,
326    ) -> Result<TransactionRequest, TransactionRequestError> {
327        if payment_data
328            .assets()
329            .iter()
330            .all(|asset| asset.is_fungible() && asset.unwrap_fungible().amount() == 0)
331        {
332            return Err(TransactionRequestError::P2IDNoteWithoutAsset);
333        }
334
335        let created_note = payment_data.into_note(note_type, rng)?;
336
337        self.own_output_notes(vec![OutputNote::Full(created_note)]).build()
338    }
339
340    /// Consumes the builder and returns a [`TransactionRequest`] for a transaction to send a SWAP
341    /// note. This request must be executed against the wallet sender account.
342    ///
343    /// - `swap_data` is the data for the swap transaction that contains the sender account ID, the
344    ///   offered asset, and the requested asset.
345    /// - `note_type` determines the visibility of the note to be created.
346    /// - `payback_note_type` determines the visibility of the payback note.
347    /// - `rng` is the random number generator used to generate the serial number for the created
348    ///   note.
349    ///
350    /// This function cannot be used with a previously set custom script.
351    pub fn build_swap(
352        self,
353        swap_data: &SwapTransactionData,
354        note_type: NoteType,
355        payback_note_type: NoteType,
356        rng: &mut ClientRng,
357    ) -> Result<TransactionRequest, TransactionRequestError> {
358        // The created note is the one that we need as the output of the tx, the other one is the
359        // one that we expect to receive and consume eventually.
360        let (created_note, payback_note_details) = create_swap_note(
361            swap_data.account_id(),
362            swap_data.offered_asset(),
363            swap_data.requested_asset(),
364            note_type,
365            Felt::ZERO,
366            payback_note_type,
367            Felt::ZERO,
368            rng,
369        )?;
370
371        let payback_tag = NoteTag::from_account_id(swap_data.account_id());
372
373        self.expected_future_notes(vec![(payback_note_details, payback_tag)])
374            .own_output_notes(vec![OutputNote::Full(created_note)])
375            .build()
376    }
377
378    // FINALIZE BUILDER
379    // --------------------------------------------------------------------------------------------
380
381    /// Consumes the builder and returns a [`TransactionRequest`].
382    ///
383    /// # Errors
384    /// - If both a custom script and own output notes are set.
385    /// - If an expiration delta is set when a custom script is set.
386    /// - If an invalid note variant is encountered in the own output notes.
387    pub fn build(self) -> Result<TransactionRequest, TransactionRequestError> {
388        let mut seen_input_notes = BTreeSet::new();
389        for (note_id, _) in &self.input_notes {
390            if !seen_input_notes.insert(note_id) {
391                return Err(TransactionRequestError::DuplicateInputNote(*note_id));
392            }
393        }
394
395        let script_template = match (self.custom_script, self.own_output_notes.is_empty()) {
396            (Some(_), false) => {
397                return Err(TransactionRequestError::ScriptTemplateError(
398                    "Cannot set both a custom script and own output notes".to_string(),
399                ));
400            },
401            (Some(script), true) => {
402                if self.expiration_delta.is_some() {
403                    return Err(TransactionRequestError::ScriptTemplateError(
404                        "Cannot set expiration delta when a custom script is set".to_string(),
405                    ));
406                }
407
408                Some(TransactionScriptTemplate::CustomScript(script))
409            },
410            (None, false) => {
411                let partial_notes = self
412                    .own_output_notes
413                    .into_iter()
414                    .map(|note| match note {
415                        OutputNote::Header(_) => Err(TransactionRequestError::InvalidNoteVariant),
416                        OutputNote::Partial(note) => Ok(note),
417                        OutputNote::Full(note) => Ok(note.into()),
418                    })
419                    .collect::<Result<Vec<PartialNote>, _>>()?;
420
421                Some(TransactionScriptTemplate::SendNotes(partial_notes))
422            },
423            (None, true) => None,
424        };
425
426        Ok(TransactionRequest {
427            unauthenticated_input_notes: self.unauthenticated_input_notes,
428            input_notes: self.input_notes,
429            script_template,
430            expected_output_recipients: self.expected_output_recipients,
431            expected_future_notes: self.expected_future_notes,
432            advice_map: self.advice_map,
433            merkle_store: self.merkle_store,
434            foreign_accounts: self.foreign_accounts.into_values().collect(),
435            expiration_delta: self.expiration_delta,
436            ignore_invalid_input_notes: self.ignore_invalid_input_notes,
437            script_arg: self.script_arg,
438            auth_arg: self.auth_arg,
439        })
440    }
441}
442
443// PAYMENT NOTE DESCRIPTION
444// ================================================================================================
445
446/// Contains information needed to create a payment note.
447#[derive(Clone, Debug)]
448pub struct PaymentNoteDescription {
449    /// Assets that are meant to be sent to the target account.
450    assets: Vec<Asset>,
451    /// Account ID of the sender account.
452    sender_account_id: AccountId,
453    /// Account ID of the receiver account.
454    target_account_id: AccountId,
455    /// Optional reclaim height for the P2IDE note. It allows the possibility for the sender to
456    /// reclaim the assets if the note has not been consumed by the target before this height.
457    reclaim_height: Option<BlockNumber>,
458    /// Optional timelock height for the P2IDE note. It allows the possibility to add a timelock to
459    /// the asset transfer, meaning that the note can only be consumed after this height.
460    timelock_height: Option<BlockNumber>,
461}
462
463impl PaymentNoteDescription {
464    // CONSTRUCTORS
465    // --------------------------------------------------------------------------------------------
466
467    /// Creates a new [`PaymentNoteDescription`].
468    pub fn new(
469        assets: Vec<Asset>,
470        sender_account_id: AccountId,
471        target_account_id: AccountId,
472    ) -> PaymentNoteDescription {
473        PaymentNoteDescription {
474            assets,
475            sender_account_id,
476            target_account_id,
477            reclaim_height: None,
478            timelock_height: None,
479        }
480    }
481
482    /// Modifies the [`PaymentNoteDescription`] to set a reclaim height for payment note.
483    #[must_use]
484    pub fn with_reclaim_height(mut self, reclaim_height: BlockNumber) -> PaymentNoteDescription {
485        self.reclaim_height = Some(reclaim_height);
486        self
487    }
488
489    /// Modifies the [`PaymentNoteDescription`] to set a timelock height for payment note.
490    #[must_use]
491    pub fn with_timelock_height(mut self, timelock_height: BlockNumber) -> PaymentNoteDescription {
492        self.timelock_height = Some(timelock_height);
493        self
494    }
495
496    /// Returns the executor [`AccountId`].
497    pub fn account_id(&self) -> AccountId {
498        self.sender_account_id
499    }
500
501    /// Returns the target [`AccountId`].
502    pub fn target_account_id(&self) -> AccountId {
503        self.target_account_id
504    }
505
506    /// Returns the transaction's list of [`Asset`].
507    pub fn assets(&self) -> &Vec<Asset> {
508        &self.assets
509    }
510
511    /// Returns the reclaim height for the P2IDE note, if set.
512    pub fn reclaim_height(&self) -> Option<BlockNumber> {
513        self.reclaim_height
514    }
515
516    /// Returns the timelock height for the P2IDE note, if set.
517    pub fn timelock_height(&self) -> Option<BlockNumber> {
518        self.timelock_height
519    }
520
521    // CONVERSION
522    // --------------------------------------------------------------------------------------------
523
524    /// Converts the payment transaction data into a [`Note`] based on the specified fields. If the
525    /// reclaim and timelock heights are not set, a P2ID note is created; otherwise, a P2IDE note is
526    /// created.
527    pub(crate) fn into_note(
528        self,
529        note_type: NoteType,
530        rng: &mut ClientRng,
531    ) -> Result<Note, NoteError> {
532        if self.reclaim_height.is_none() && self.timelock_height.is_none() {
533            // Create a P2ID note
534            create_p2id_note(
535                self.sender_account_id,
536                self.target_account_id,
537                self.assets,
538                note_type,
539                Felt::ZERO,
540                rng,
541            )
542        } else {
543            // Create a P2IDE note
544            create_p2ide_note(
545                self.sender_account_id,
546                self.target_account_id,
547                self.assets,
548                self.reclaim_height,
549                self.timelock_height,
550                note_type,
551                Felt::ZERO,
552                rng,
553            )
554        }
555    }
556}
557
558// SWAP TRANSACTION DATA
559// ================================================================================================
560
561/// Contains information related to a swap transaction.
562///
563/// A swap transaction involves creating a SWAP note, which will carry the offered asset and which,
564/// when consumed, will create a payback note that carries the requested asset taken from the
565/// consumer account's vault.
566#[derive(Clone, Debug)]
567pub struct SwapTransactionData {
568    /// Account ID of the sender account.
569    sender_account_id: AccountId,
570    /// Asset that is offered in the swap.
571    offered_asset: Asset,
572    /// Asset that is expected in the payback note generated as a result of the swap.
573    requested_asset: Asset,
574}
575
576impl SwapTransactionData {
577    // CONSTRUCTORS
578    // --------------------------------------------------------------------------------------------
579
580    /// Creates a new [`SwapTransactionData`].
581    pub fn new(
582        sender_account_id: AccountId,
583        offered_asset: Asset,
584        requested_asset: Asset,
585    ) -> SwapTransactionData {
586        SwapTransactionData {
587            sender_account_id,
588            offered_asset,
589            requested_asset,
590        }
591    }
592
593    /// Returns the executor [`AccountId`].
594    pub fn account_id(&self) -> AccountId {
595        self.sender_account_id
596    }
597
598    /// Returns the transaction offered [`Asset`].
599    pub fn offered_asset(&self) -> Asset {
600        self.offered_asset
601    }
602
603    /// Returns the transaction requested [`Asset`].
604    pub fn requested_asset(&self) -> Asset {
605        self.requested_asset
606    }
607}