Skip to main content

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}