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