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, AssetAmount, 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 NoteDetailsCommitment,
19 NoteId,
20 NoteRecipient,
21 NoteScript,
22 NoteStorage,
23 NoteTag,
24 NoteType,
25 PartialNote,
26 PartialNoteMetadata,
27};
28use miden_protocol::transaction::TransactionScript;
29use miden_protocol::vm::AdviceMap;
30use miden_protocol::{Felt, Word};
31use miden_standards::note::{P2idNote, P2ideNote, PswapNote, PswapNoteStorage, SwapNote};
32
33use super::{
34 ForeignAccount,
35 NoteArgs,
36 TransactionRequest,
37 TransactionRequestError,
38 TransactionScriptTemplate,
39};
40use crate::ClientRng;
41
42// TRANSACTION REQUEST BUILDER
43// ================================================================================================
44
45/// A builder for a [`TransactionRequest`].
46///
47/// Use this builder to construct a [`TransactionRequest`] by adding input notes, specifying
48/// scripts, and setting other transaction parameters.
49#[derive(Clone, Debug)]
50pub struct TransactionRequestBuilder {
51 /// Notes to be consumed by the transaction.
52 /// Notes whose inclusion proof is present in the store are will be consumed as authenticated;
53 /// the ones that do not have proofs will be consumed as unauthenticated.
54 input_notes: Vec<Note>,
55 /// Optional arguments of the Notes to be consumed by the transaction. This
56 /// includes both authenticated and unauthenticated notes.
57 input_notes_args: Vec<(NoteId, Option<NoteArgs>)>,
58 /// Notes to be created by the transaction. The full note data is needed internally
59 /// to build the transaction script template.
60 own_output_notes: Vec<Note>,
61 /// A map of recipients of the output notes expected to be generated by the transaction.
62 expected_output_recipients: BTreeMap<Word, NoteRecipient>,
63 /// A map of details and tags of notes we expect to be created as part of future transactions
64 /// with their respective tags.
65 ///
66 /// For example, after a swap note is consumed, a payback note is expected to be created.
67 expected_future_notes: BTreeMap<NoteDetailsCommitment, (NoteDetails, NoteTag)>,
68 /// Custom transaction script to be used.
69 custom_script: Option<TransactionScript>,
70 /// Initial state of the `AdviceMap` that provides data during runtime.
71 advice_map: AdviceMap,
72 /// Initial state of the `MerkleStore` that provides data during runtime.
73 merkle_store: MerkleStore,
74 /// Foreign account data requirements. At execution time, account data will be retrieved from
75 /// the network, and injected as advice inputs. Additionally, the account's code will be
76 /// added to the executor and prover.
77 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
78 /// The number of blocks in relation to the transaction's reference block after which the
79 /// transaction will expire. If `None`, the transaction will not expire.
80 expiration_delta: Option<u16>,
81 /// Indicates whether to **silently** ignore invalid input notes when executing the
82 /// transaction. This will allow the transaction to be executed even if some input notes
83 /// are invalid.
84 ignore_invalid_input_notes: bool,
85 /// Optional [`Word`] that will be pushed to the operand stack before the transaction script
86 /// execution. If the advice map is extended with some user defined entries, this script
87 /// argument could be used as a key to access the corresponding value.
88 script_arg: Option<Word>,
89 /// Optional [`Word`] that will be pushed to the stack for the authentication procedure
90 /// during transaction execution.
91 auth_arg: Option<Word>,
92 /// Salt the native fee conversion info is committed under when the transaction is prepared,
93 /// set through [`TransactionRequestBuilder::fee_conversion_salt`]. `None` leaves the client
94 /// to use its fixed default salt.
95 fee_conversion_salt: Option<Word>,
96 /// Note scripts that the node's NTX builder will need in its script registry.
97 ///
98 /// See [`TransactionRequestBuilder::expected_ntx_scripts`] for details.
99 expected_ntx_scripts: Vec<NoteScript>,
100}
101
102impl TransactionRequestBuilder {
103 // CONSTRUCTORS
104 // --------------------------------------------------------------------------------------------
105
106 /// Creates a new, empty [`TransactionRequestBuilder`].
107 pub fn new() -> Self {
108 Self {
109 input_notes: vec![],
110 input_notes_args: vec![],
111 own_output_notes: Vec::new(),
112 expected_output_recipients: BTreeMap::new(),
113 expected_future_notes: BTreeMap::new(),
114 custom_script: None,
115 advice_map: AdviceMap::default(),
116 merkle_store: MerkleStore::default(),
117 expiration_delta: None,
118 foreign_accounts: BTreeMap::default(),
119 ignore_invalid_input_notes: false,
120 script_arg: None,
121 auth_arg: None,
122 fee_conversion_salt: None,
123 expected_ntx_scripts: vec![],
124 }
125 }
126
127 /// Adds the specified notes as input notes to the transaction request.
128 #[must_use]
129 pub fn input_notes(
130 mut self,
131 notes: impl IntoIterator<Item = (Note, Option<NoteArgs>)>,
132 ) -> Self {
133 for (note, argument) in notes {
134 self.input_notes_args.push((note.id(), argument));
135 self.input_notes.push(note);
136 }
137 self
138 }
139
140 /// Specifies the output notes that should be created in the transaction script and will
141 /// be used as a transaction script template. These notes will also be added to the expected
142 /// output recipients of the transaction.
143 ///
144 /// If a transaction script template is already set (e.g. by calling `with_custom_script`), the
145 /// [`TransactionRequestBuilder::build`] method will return an error.
146 #[must_use]
147 pub fn own_output_notes(mut self, notes: impl IntoIterator<Item = Note>) -> Self {
148 for note in notes {
149 self.expected_output_recipients
150 .insert(note.recipient().digest(), note.recipient().clone());
151 self.own_output_notes.push(note);
152 }
153
154 self
155 }
156
157 /// Specifies a custom transaction script to be used.
158 ///
159 /// If a script template is already set (e.g. by calling `with_own_output_notes`), the
160 /// [`TransactionRequestBuilder::build`] method will return an error.
161 #[must_use]
162 pub fn custom_script(mut self, script: TransactionScript) -> Self {
163 self.custom_script = Some(script);
164 self
165 }
166
167 /// Specifies one or more foreign accounts (public or private) that contain data
168 /// utilized by the transaction.
169 ///
170 /// At execution, the client queries the node and retrieves the appropriate data,
171 /// depending on whether each foreign account is public or private:
172 ///
173 /// - **Public accounts**: the node retrieves the state and code for the account and injects
174 /// them as advice inputs. Public accounts can be omitted here, as they will be lazily loaded
175 /// through RPC calls. Undeclared accounts may trigger additional RPC calls for storage map
176 /// accesses during execution.
177 /// - **Private accounts**: the node retrieves a proof of the account's existence and injects
178 /// that as advice inputs. Private accounts must always be declared here with their
179 /// [`PartialAccount`](miden_protocol::account::PartialAccount) state.
180 #[must_use]
181 pub fn foreign_accounts(
182 mut self,
183 foreign_accounts: impl IntoIterator<Item = impl Into<ForeignAccount>>,
184 ) -> Self {
185 for account in foreign_accounts {
186 let foreign_account: ForeignAccount = account.into();
187 self.foreign_accounts.insert(foreign_account.account_id(), foreign_account);
188 }
189
190 self
191 }
192
193 /// Specifies a transaction's expected output note recipients.
194 ///
195 /// The set of specified recipients is treated as a subset of the recipients for notes that may
196 /// be created by a transaction. That is, the transaction must create notes for all the
197 /// specified expected recipients, but it may also create notes for other recipients not
198 /// included in this set.
199 #[must_use]
200 pub fn expected_output_recipients(mut self, recipients: Vec<NoteRecipient>) -> Self {
201 self.expected_output_recipients = recipients
202 .into_iter()
203 .map(|recipient| (recipient.digest(), recipient))
204 .collect::<BTreeMap<_, _>>();
205 self
206 }
207
208 /// Specifies a set of notes which may be created when a transaction's output notes are
209 /// consumed.
210 ///
211 /// For example, after a SWAP note is consumed, a payback note is expected to be created. This
212 /// allows the client to track this note accordingly.
213 #[must_use]
214 pub fn expected_future_notes(mut self, notes: Vec<(NoteDetails, NoteTag)>) -> Self {
215 self.expected_future_notes = notes
216 .into_iter()
217 .map(|note| (note.0.commitment(), note))
218 .collect::<BTreeMap<_, _>>();
219 self
220 }
221
222 /// Extends the advice map with the specified `([Word], Vec<[Felt]>)` pairs.
223 #[must_use]
224 pub fn extend_advice_map<I, V>(mut self, iter: I) -> Self
225 where
226 I: IntoIterator<Item = (Word, V)>,
227 V: AsRef<[Felt]>,
228 {
229 self.advice_map.extend(iter.into_iter().map(|(w, v)| (w, v.as_ref().to_vec())));
230 self
231 }
232
233 /// Extends the merkle store with the specified [`InnerNodeInfo`] elements.
234 #[must_use]
235 pub fn extend_merkle_store<T: IntoIterator<Item = InnerNodeInfo>>(mut self, iter: T) -> Self {
236 self.merkle_store.extend(iter);
237 self
238 }
239
240 /// The number of blocks in relation to the transaction's reference block after which the
241 /// transaction will expire. By default, the transaction will not expire.
242 ///
243 /// Setting transaction expiration delta defines an upper bound for transaction expiration,
244 /// but other code executed during the transaction may impose an even smaller transaction
245 /// expiration delta.
246 #[must_use]
247 pub fn expiration_delta(mut self, expiration_delta: u16) -> Self {
248 self.expiration_delta = Some(expiration_delta);
249 self
250 }
251
252 /// The resulting transaction will **silently** ignore invalid input notes when being executed.
253 /// By default, this will not happen.
254 #[must_use]
255 pub fn ignore_invalid_input_notes(mut self) -> Self {
256 self.ignore_invalid_input_notes = true;
257 self
258 }
259
260 /// Sets an optional [`Word`] that will be pushed to the operand stack before the transaction
261 /// script execution. If the advice map is extended with some user defined entries, this script
262 /// argument could be used as a key to access the corresponding value.
263 #[must_use]
264 pub fn script_arg(mut self, script_arg: Word) -> Self {
265 self.script_arg = Some(script_arg);
266 self
267 }
268
269 /// Sets an optional [`Word`] that will be pushed to the stack for the authentication
270 /// procedure during transaction execution.
271 #[must_use]
272 pub fn auth_arg(mut self, auth_arg: Word) -> Self {
273 self.auth_arg = Some(auth_arg);
274 self.fee_conversion_salt = None;
275 self
276 }
277
278 /// Declares the salt the fee conversion info is committed under.
279 ///
280 /// Fees are always settled in the chain's native fee asset at rate 1/1. The client commits
281 /// that info through the transaction's auth args when preparing the transaction, under a
282 /// fixed default salt.
283 #[must_use]
284 pub fn fee_conversion_salt(mut self, salt: Word) -> Self {
285 self.fee_conversion_salt = Some(salt);
286 self.auth_arg = None;
287 self
288 }
289
290 /// Specifies note scripts that the node's network transaction (NTX) builder will need in
291 /// its script registry.
292 ///
293 /// When a transaction creates notes destined for a network account, the node's NTX builder
294 /// must have the scripts of any public output notes in its registry. If a required script
295 /// is missing, the NTX will silently fail on the node side.
296 ///
297 /// When this field is set, the client will check each script against the node before
298 /// executing the main transaction. For any script not yet registered, the client
299 /// automatically creates and submits a separate registration transaction (a public note
300 /// carrying that script) so the node's registry is populated before the NTX executes.
301 ///
302 /// Standard note scripts are ignored here — the NTX builder resolves them directly.
303 #[must_use]
304 pub fn expected_ntx_scripts(mut self, scripts: Vec<NoteScript>) -> Self {
305 self.expected_ntx_scripts = scripts;
306 self
307 }
308
309 // STANDARDIZED REQUESTS
310 // --------------------------------------------------------------------------------------------
311
312 /// Consumes the builder and returns a [`TransactionRequest`] for a transaction to consume the
313 /// specified notes.
314 ///
315 /// - `notes` is a list of notes to be consumed.
316 pub fn build_consume_notes(
317 self,
318 notes: Vec<Note>,
319 ) -> Result<TransactionRequest, TransactionRequestError> {
320 let input_notes = notes.into_iter().map(|id| (id, None));
321 self.input_notes(input_notes).build()
322 }
323
324 /// Consumes the builder and returns a [`TransactionRequest`] for a transaction to mint fungible
325 /// assets. This request must be executed against a fungible faucet account.
326 ///
327 /// - `asset` is the fungible asset to be minted. The amount must be non-zero: minting nothing
328 /// would emit a P2ID note the target cannot draw anything from.
329 /// - `target_id` is the account ID of the account to receive the minted asset.
330 /// - `note_type` determines the visibility of the note to be created.
331 /// - `rng` is the random number generator used to generate the serial number for the created
332 /// note.
333 ///
334 /// This function cannot be used with a previously set custom script.
335 pub fn build_mint_fungible_asset(
336 self,
337 asset: FungibleAsset,
338 target_id: AccountId,
339 note_type: NoteType,
340 rng: &mut ClientRng,
341 ) -> Result<TransactionRequest, TransactionRequestError> {
342 // Minting emits a P2ID note, and a P2ID note carrying nothing is rejected on the transfer
343 // path for the same reason: it costs a transaction and leaves the target a note with
344 // nothing to consume.
345 if asset.amount() == AssetAmount::ZERO {
346 return Err(TransactionRequestError::P2IDNoteWithoutAsset);
347 }
348
349 let created_note = P2idNote::builder()
350 .sender(asset.faucet_id())
351 .target(target_id)
352 .asset(asset)
353 .note_type(note_type)
354 .generate_serial_number(rng)
355 .build()?
356 .into();
357
358 self.own_output_notes(vec![created_note]).build()
359 }
360
361 /// Consumes the builder and returns a [`TransactionRequest`] for a transaction to send a P2ID
362 /// or P2IDE note. This request must be executed against the wallet sender account.
363 ///
364 /// - `payment_data` is the data for the payment transaction that contains the asset to be
365 /// transferred, the sender account ID, and the target account ID. If the recall or timelock
366 /// heights are set, a P2IDE note will be created; otherwise, a P2ID note will be created.
367 /// - `note_type` determines the visibility of the note to be created.
368 /// - `rng` is the random number generator used to generate the serial number for the created
369 /// note.
370 ///
371 /// This function cannot be used with a previously set custom script.
372 pub fn build_pay_to_id(
373 self,
374 payment_data: PaymentNoteDescription,
375 note_type: NoteType,
376 rng: &mut ClientRng,
377 ) -> Result<TransactionRequest, TransactionRequestError> {
378 if payment_data
379 .assets()
380 .iter()
381 .all(|asset| asset.is_fungible() && asset.unwrap_fungible().amount().as_u64() == 0)
382 {
383 return Err(TransactionRequestError::P2IDNoteWithoutAsset);
384 }
385
386 let created_note = payment_data.into_note(note_type, rng)?;
387
388 self.own_output_notes(vec![created_note]).build()
389 }
390
391 /// Consumes the builder and returns a [`TransactionRequest`] for a transaction to send a SWAP
392 /// note. This request must be executed against the wallet sender account.
393 ///
394 /// - `swap_data` is the data for the swap transaction that contains the sender account ID, the
395 /// offered asset, and the requested asset.
396 /// - `note_type` determines the visibility of the note to be created.
397 /// - `payback_note_type` determines the visibility of the payback note.
398 /// - `rng` is the random number generator used to generate the serial number for the created
399 /// note.
400 ///
401 /// This function cannot be used with a previously set custom script.
402 pub fn build_swap(
403 self,
404 swap_data: &SwapTransactionData,
405 note_type: NoteType,
406 payback_note_type: NoteType,
407 rng: &mut ClientRng,
408 ) -> Result<TransactionRequest, TransactionRequestError> {
409 // The created note is the one that we need as the output of the tx, the other one is the
410 // one that we expect to receive and consume eventually.
411 let swap_note = SwapNote::builder()
412 .sender(swap_data.account_id())
413 .offered_asset(swap_data.offered_asset())
414 .requested_asset(swap_data.requested_asset())
415 .note_type(note_type)
416 .payback_note_type(payback_note_type)
417 .generate_serial_number(rng)
418 .build()?;
419
420 let payback_note_details = swap_note.payback_note_details();
421 let created_note = Note::from(swap_note);
422
423 let payback_tag = NoteTag::with_account_target(swap_data.account_id());
424
425 self.expected_future_notes(vec![(payback_note_details, payback_tag)])
426 .own_output_notes(vec![created_note])
427 .build()
428 }
429
430 /// Consumes the builder and returns a [`TransactionRequest`] for a transaction that registers
431 /// note scripts in the node's script registry.
432 ///
433 /// This creates one public output note per script, each with empty assets and storage. The
434 /// node indexes the script of every public note it processes, so submitting this transaction
435 /// makes the scripts available for future network transactions (NTX).
436 ///
437 /// - `sender_account_id` is the account executing the transaction.
438 /// - `scripts` is the list of note scripts to register.
439 /// - `rng` is used to generate serial numbers for the registration notes.
440 ///
441 /// This function cannot be used with a previously set custom script.
442 pub fn build_register_note_scripts(
443 self,
444 sender_account_id: AccountId,
445 scripts: Vec<NoteScript>,
446 rng: &mut ClientRng,
447 ) -> Result<TransactionRequest, TransactionRequestError> {
448 let registration_notes: Vec<Note> = scripts
449 .into_iter()
450 .map(|script| {
451 let serial_num = rng.draw_word();
452 let note_storage = NoteStorage::new(vec![])?;
453 let recipient = NoteRecipient::new(serial_num, script, note_storage);
454 let note_assets = NoteAssets::new(vec![])?;
455 let metadata = PartialNoteMetadata::new(sender_account_id, NoteType::Public);
456 Ok(Note::new(note_assets, metadata, recipient))
457 })
458 .collect::<Result<_, NoteError>>()?;
459
460 self.own_output_notes(registration_notes).build()
461 }
462
463 /// Consumes the builder and returns a [`TransactionRequest`] for a transaction that creates a
464 /// partial swap (PSWAP) note. This request must be executed against the creator account.
465 ///
466 /// - `pswap_data` is the data for the partial swap that contains the creator account ID, the
467 /// offered fungible asset, and the requested fungible asset.
468 /// - `note_type` determines the visibility of the PSWAP note itself.
469 /// - `payback_note_type` determines the visibility of the payback note that fillers emit back
470 /// to the creator. Typically [`NoteType::Private`] (cheaper; the fill amount is already
471 /// visible in the executing transaction).
472 /// - `note_attachment` is the optional attachment for the PSWAP note. Pass `None` when there is
473 /// nothing to attach.
474 /// - `rng` is the random number generator used to generate the serial number for the created
475 /// note.
476 ///
477 /// This function cannot be used with a previously set custom script.
478 pub fn build_pswap_create(
479 self,
480 pswap_data: &PswapTransactionData,
481 note_type: NoteType,
482 payback_note_type: NoteType,
483 note_attachment: Option<NoteAttachment>,
484 rng: &mut ClientRng,
485 ) -> Result<TransactionRequest, TransactionRequestError> {
486 let storage = PswapNoteStorage::builder()
487 .min_requested_asset(pswap_data.requested_asset())
488 .creator_account_id(pswap_data.creator_account_id())
489 .payback_note_type(payback_note_type)
490 .build();
491
492 let pswap_note = PswapNote::builder()
493 .sender(pswap_data.creator_account_id())
494 .storage(storage)
495 .serial_number(rng.draw_word())
496 .note_type(note_type)
497 .offered_asset(pswap_data.offered_asset())
498 .maybe_attachment(note_attachment)
499 .build()
500 .map_err(TransactionRequestError::NoteCreationError)?;
501
502 let note: Note = pswap_note.into();
503 self.own_output_notes(vec![note]).build()
504 }
505
506 /// Consumes the builder and returns a [`TransactionRequest`] for a transaction that consumes
507 /// (fills) a partial swap (PSWAP) note. This request must be executed against the consumer
508 /// account.
509 ///
510 /// - `pswap_note` is the PSWAP note being consumed.
511 /// - `consumer_account_id` is the account consuming the swap.
512 /// - `account_fill_amount` is the amount of the requested asset being provided by the consumer
513 /// account.
514 /// - `note_fill_amount` is any additional amount being provided by other (in-flight) notes.
515 ///
516 /// This function cannot be used with a previously set custom script.
517 pub fn build_pswap_consume(
518 self,
519 pswap_note: &Note,
520 consumer_account_id: AccountId,
521 account_fill_amount: AssetAmount,
522 note_fill_amount: AssetAmount,
523 ) -> Result<TransactionRequest, TransactionRequestError> {
524 let pswap = PswapNote::try_from(pswap_note)
525 .map_err(TransactionRequestError::NoteValidationError)?;
526
527 let requested_faucet_id = pswap.storage().min_requested_asset().faucet_id();
528
529 let account_fill_asset =
530 FungibleAsset::new(requested_faucet_id, account_fill_amount.as_u64())?;
531 let note_fill_asset = FungibleAsset::new(requested_faucet_id, note_fill_amount.as_u64())?;
532
533 let (payback_note, remainder_pswap) = pswap
534 .execute(consumer_account_id, Some(account_fill_asset), Some(note_fill_asset))
535 .map_err(TransactionRequestError::NoteExecutionError)?;
536
537 let note_args =
538 PswapNote::create_args(account_fill_amount.as_u64(), note_fill_amount.as_u64())
539 .map_err(TransactionRequestError::NoteArgError)?;
540
541 // Payback and remainder both settle to the creator, not the consumer. Declare them as
542 // expected recipients so the transaction is validated against them, but don't register
543 // them as expected future notes — that's the creator's concern, and doing so here would
544 // leave stale, un-consumable notes in the consumer's store.
545 let mut expected_recipients = vec![payback_note.recipient().clone()];
546
547 if let Some(remainder) = remainder_pswap {
548 let remainder_note: Note = remainder.into();
549 expected_recipients.push(remainder_note.recipient().clone());
550 }
551
552 self.input_notes(vec![(pswap_note.clone(), Some(note_args))])
553 .expected_output_recipients(expected_recipients)
554 .build()
555 }
556
557 /// Consumes the builder and returns a [`TransactionRequest`] for a transaction that cancels a
558 /// partial swap (PSWAP) note. This request must be executed against the creator account.
559 ///
560 /// - `pswap_note` is the PSWAP note to cancel.
561 /// - `creator_account_id` is the account that created the note. The note's stored creator must
562 /// match this ID; this is the account the resulting transaction must be executed against.
563 ///
564 /// This function cannot be used with a previously set custom script.
565 pub fn build_pswap_cancel(
566 self,
567 pswap_note: Note,
568 creator_account_id: AccountId,
569 ) -> Result<TransactionRequest, TransactionRequestError> {
570 let pswap = PswapNote::try_from(&pswap_note)
571 .map_err(TransactionRequestError::NoteValidationError)?;
572
573 let note_creator = pswap.storage().creator_account_id();
574 if note_creator != creator_account_id {
575 return Err(TransactionRequestError::PswapCancelCreatorMismatch {
576 expected: note_creator,
577 actual: creator_account_id,
578 });
579 }
580
581 self.input_notes(vec![(pswap_note, None)]).build()
582 }
583
584 // FINALIZE BUILDER
585 // --------------------------------------------------------------------------------------------
586
587 /// Consumes the builder and returns a [`TransactionRequest`].
588 ///
589 /// # Errors
590 /// - If both a custom script and own output notes are set.
591 /// - If an expiration delta is set when a custom script is set.
592 /// - If an invalid note variant is encountered in the own output notes.
593 pub fn build(self) -> Result<TransactionRequest, TransactionRequestError> {
594 let mut seen_input_notes = BTreeSet::new();
595 for (note_id, _) in &self.input_notes_args {
596 if !seen_input_notes.insert(note_id) {
597 return Err(TransactionRequestError::DuplicateInputNote(*note_id));
598 }
599 }
600
601 if self.expiration_delta == Some(0) {
602 return Err(TransactionRequestError::ZeroExpirationDelta);
603 }
604
605 let script_template = match (self.custom_script, self.own_output_notes.is_empty()) {
606 (Some(_), false) => {
607 return Err(TransactionRequestError::ScriptTemplateError(
608 "Cannot set both a custom script and own output notes".to_string(),
609 ));
610 },
611 (Some(script), true) => {
612 if self.expiration_delta.is_some() {
613 return Err(TransactionRequestError::ScriptTemplateError(
614 "Cannot set expiration delta when a custom script is set".to_string(),
615 ));
616 }
617
618 Some(TransactionScriptTemplate::CustomScript(script))
619 },
620 (None, false) => {
621 let partial_notes: Vec<PartialNote> =
622 self.own_output_notes.into_iter().map(Into::into).collect();
623
624 Some(TransactionScriptTemplate::SendNotes(partial_notes))
625 },
626 (None, true) => None,
627 };
628
629 Ok(TransactionRequest {
630 input_notes: self.input_notes,
631 input_notes_args: self.input_notes_args,
632 script_template,
633 expected_output_recipients: self.expected_output_recipients,
634 expected_future_notes: self.expected_future_notes,
635 advice_map: self.advice_map,
636 merkle_store: self.merkle_store,
637 foreign_accounts: self.foreign_accounts,
638 expiration_delta: self.expiration_delta,
639 ignore_invalid_input_notes: self.ignore_invalid_input_notes,
640 script_arg: self.script_arg,
641 auth_arg: self.auth_arg,
642 fee_conversion_salt: self.fee_conversion_salt,
643 expected_ntx_scripts: self.expected_ntx_scripts,
644 })
645 }
646}
647
648// PAYMENT NOTE DESCRIPTION
649// ================================================================================================
650
651/// Contains information needed to create a payment note.
652#[derive(Clone, Debug)]
653pub struct PaymentNoteDescription {
654 /// Assets that are meant to be sent to the target account.
655 assets: Vec<Asset>,
656 /// Account ID of the sender account.
657 sender_account_id: AccountId,
658 /// Account ID of the receiver account.
659 target_account_id: AccountId,
660 /// Optional reclaim height for the P2IDE note. It allows the possibility for the sender to
661 /// reclaim the assets if the note has not been consumed by the target before this height.
662 reclaim_height: Option<BlockNumber>,
663 /// Optional timelock height for the P2IDE note. It allows the possibility to add a timelock to
664 /// the asset transfer, meaning that the note can only be consumed after this height.
665 timelock_height: Option<BlockNumber>,
666}
667
668impl PaymentNoteDescription {
669 // CONSTRUCTORS
670 // --------------------------------------------------------------------------------------------
671
672 /// Creates a new [`PaymentNoteDescription`].
673 pub fn new(
674 assets: Vec<Asset>,
675 sender_account_id: AccountId,
676 target_account_id: AccountId,
677 ) -> PaymentNoteDescription {
678 PaymentNoteDescription {
679 assets,
680 sender_account_id,
681 target_account_id,
682 reclaim_height: None,
683 timelock_height: None,
684 }
685 }
686
687 /// Modifies the [`PaymentNoteDescription`] to set a reclaim height for payment note.
688 #[must_use]
689 pub fn with_reclaim_height(mut self, reclaim_height: BlockNumber) -> PaymentNoteDescription {
690 self.reclaim_height = Some(reclaim_height);
691 self
692 }
693
694 /// Modifies the [`PaymentNoteDescription`] to set a timelock height for payment note.
695 #[must_use]
696 pub fn with_timelock_height(mut self, timelock_height: BlockNumber) -> PaymentNoteDescription {
697 self.timelock_height = Some(timelock_height);
698 self
699 }
700
701 /// Returns the executor [`AccountId`].
702 pub fn account_id(&self) -> AccountId {
703 self.sender_account_id
704 }
705
706 /// Returns the target [`AccountId`].
707 pub fn target_account_id(&self) -> AccountId {
708 self.target_account_id
709 }
710
711 /// Returns the transaction's list of [`Asset`].
712 pub fn assets(&self) -> &Vec<Asset> {
713 &self.assets
714 }
715
716 /// Returns the reclaim height for the P2IDE note, if set.
717 pub fn reclaim_height(&self) -> Option<BlockNumber> {
718 self.reclaim_height
719 }
720
721 /// Returns the timelock height for the P2IDE note, if set.
722 pub fn timelock_height(&self) -> Option<BlockNumber> {
723 self.timelock_height
724 }
725
726 // CONVERSION
727 // --------------------------------------------------------------------------------------------
728
729 /// Converts the payment transaction data into a [`Note`] based on the specified fields. If the
730 /// reclaim and timelock heights are not set, a P2ID note is created; otherwise, a P2IDE note is
731 /// created.
732 pub(crate) fn into_note(
733 self,
734 note_type: NoteType,
735 rng: &mut ClientRng,
736 ) -> Result<Note, NoteError> {
737 if self.reclaim_height.is_none() && self.timelock_height.is_none() {
738 // Create a P2ID note
739 Ok(P2idNote::builder()
740 .sender(self.sender_account_id)
741 .target(self.target_account_id)
742 .assets(self.assets)
743 .note_type(note_type)
744 .generate_serial_number(rng)
745 .build()?
746 .into())
747 } else {
748 // Create a P2IDE note
749 Ok(P2ideNote::builder()
750 .sender(self.sender_account_id)
751 .target(self.target_account_id)
752 .assets(self.assets)
753 .note_type(note_type)
754 .maybe_reclaim_height(self.reclaim_height)
755 .maybe_timelock_height(self.timelock_height)
756 .generate_serial_number(rng)
757 .build()?
758 .into())
759 }
760 }
761}
762
763// SWAP TRANSACTION DATA
764// ================================================================================================
765
766/// Contains information related to a swap transaction.
767///
768/// A swap transaction involves creating a SWAP note, which will carry the offered asset and which,
769/// when consumed, will create a payback note that carries the requested asset taken from the
770/// consumer account's vault.
771#[derive(Clone, Debug)]
772pub struct SwapTransactionData {
773 /// Account ID of the sender account.
774 sender_account_id: AccountId,
775 /// Asset that is offered in the swap.
776 offered_asset: Asset,
777 /// Asset that is expected in the payback note generated as a result of the swap.
778 requested_asset: Asset,
779}
780
781impl SwapTransactionData {
782 // CONSTRUCTORS
783 // --------------------------------------------------------------------------------------------
784
785 /// Creates a new [`SwapTransactionData`].
786 pub fn new(
787 sender_account_id: AccountId,
788 offered_asset: Asset,
789 requested_asset: Asset,
790 ) -> SwapTransactionData {
791 SwapTransactionData {
792 sender_account_id,
793 offered_asset,
794 requested_asset,
795 }
796 }
797
798 /// Returns the executor [`AccountId`].
799 pub fn account_id(&self) -> AccountId {
800 self.sender_account_id
801 }
802
803 /// Returns the transaction offered [`Asset`].
804 pub fn offered_asset(&self) -> Asset {
805 self.offered_asset
806 }
807
808 /// Returns the transaction requested [`Asset`].
809 pub fn requested_asset(&self) -> Asset {
810 self.requested_asset
811 }
812}
813
814// PSWAP TRANSACTION DATA
815// ================================================================================================
816
817/// Contains information related to a partial swap (PSWAP) transaction.
818///
819/// A PSWAP transaction involves creating a PSWAP note that carries the offered fungible asset
820/// and, when consumed (filled), produces a payback note carrying the requested fungible asset
821/// taken from the filler's vault. Both legs are restricted to fungible assets so that fills can
822/// be denominated in arbitrary amounts.
823#[derive(Clone, Debug)]
824pub struct PswapTransactionData {
825 /// Account ID of the creator account.
826 creator_account_id: AccountId,
827 /// Fungible asset offered in the swap.
828 offered_asset: FungibleAsset,
829 /// Fungible asset expected in the payback note generated when the PSWAP is filled.
830 requested_asset: FungibleAsset,
831}
832
833impl PswapTransactionData {
834 // CONSTRUCTORS
835 // --------------------------------------------------------------------------------------------
836
837 /// Creates a new [`PswapTransactionData`].
838 pub fn new(
839 creator_account_id: AccountId,
840 offered_asset: FungibleAsset,
841 requested_asset: FungibleAsset,
842 ) -> PswapTransactionData {
843 PswapTransactionData {
844 creator_account_id,
845 offered_asset,
846 requested_asset,
847 }
848 }
849
850 /// Returns the creator [`AccountId`].
851 pub fn creator_account_id(&self) -> AccountId {
852 self.creator_account_id
853 }
854
855 /// Returns the offered [`FungibleAsset`].
856 pub fn offered_asset(&self) -> FungibleAsset {
857 self.offered_asset
858 }
859
860 /// Returns the requested [`FungibleAsset`].
861 pub fn requested_asset(&self) -> FungibleAsset {
862 self.requested_asset
863 }
864}