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