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}