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