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