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) = ¬e {
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}