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