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