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}