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