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