Skip to main content

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}