Skip to main content

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