miden_standards/tx_script/send_notes_script.rs
1use alloc::vec::Vec;
2use core::num::NonZeroU16;
3
4use miden_protocol::account::{AccountCodeInterface, AccountId, AccountProcedureRoot};
5use miden_protocol::asset::AssetComposition;
6use miden_protocol::note::PartialNote;
7use miden_protocol::transaction::{TransactionScript, TransactionScriptRoot};
8use miden_protocol::utils::sync::LazyLock;
9use miden_protocol::vm::AdviceMap;
10use miden_protocol::{Felt, Hasher, Word, ZERO};
11use thiserror::Error;
12
13use crate::account::access::{Ownable2Step, RoleBasedAccessControl};
14use crate::account::faucets::{FungibleFaucet, NonFungibleFaucet};
15use crate::account::wallets::BasicWallet;
16use crate::tx_script::transaction_script;
17
18// CONSTANTS
19// ================================================================================================
20
21/// Path to the `send_notes` wallet transaction script procedure in the standards library.
22const SEND_NOTES_WALLET_TX_SCRIPT_PATH: &str =
23 "::miden::standards::tx_scripts::send_notes::wallet::main";
24
25/// Path to the `send_notes` fungible faucet transaction script procedure in the standards library.
26const SEND_NOTES_FUNGIBLE_FAUCET_TX_SCRIPT_PATH: &str =
27 "::miden::standards::tx_scripts::send_notes::fungible_faucet::main";
28
29/// Path to the `send_notes` non-fungible faucet transaction script procedure in the standards
30/// library.
31const SEND_NOTES_NON_FUNGIBLE_FAUCET_TX_SCRIPT_PATH: &str =
32 "::miden::standards::tx_scripts::send_notes::non_fungible_faucet::main";
33
34// SEND NOTES TRANSACTION SCRIPT
35// ================================================================================================
36
37static SEND_NOTES_WALLET_TX_SCRIPT: LazyLock<TransactionScript> =
38 LazyLock::new(|| transaction_script(SEND_NOTES_WALLET_TX_SCRIPT_PATH));
39
40static SEND_NOTES_FUNGIBLE_FAUCET_TX_SCRIPT: LazyLock<TransactionScript> =
41 LazyLock::new(|| transaction_script(SEND_NOTES_FUNGIBLE_FAUCET_TX_SCRIPT_PATH));
42
43static SEND_NOTES_NON_FUNGIBLE_FAUCET_TX_SCRIPT: LazyLock<TransactionScript> =
44 LazyLock::new(|| transaction_script(SEND_NOTES_NON_FUNGIBLE_FAUCET_TX_SCRIPT_PATH));
45
46/// A `send_notes` [`TransactionScript`] for an account, abstracting over the concrete script that
47/// the account's code interface calls for.
48///
49/// Each variant wraps the dedicated type for one canonical script. Use [`Self::new`] to let the
50/// account's interface decide, or construct a concrete type directly when the kind is known.
51///
52/// The script is picked from the interfaces the account exposes, cross-checked against the
53/// composition of the assets being sent: a faucet script is built only for notes its faucet can
54/// mint, and any other notes are sent with the [`BasicWallet`] script.
55///
56/// # Example
57///
58/// ```ignore
59/// let script = SendNotesTransactionScript::new(&interface, ¬es)?;
60///
61/// let tx_args = TransactionArgs::new(AdviceMap::default())
62/// .with_tx_script_and_args(script.tx_script().clone(), script.tx_script_args());
63/// ```
64#[derive(Debug, Clone)]
65#[non_exhaustive]
66pub enum SendNotesTransactionScript {
67 /// Sends notes holding assets the account already owns.
68 Wallet(SendWalletNotesTransactionScript),
69 /// Sends notes holding assets the fungible faucet mints as part of note creation.
70 Fungible(SendFungibleFaucetNotesTransactionScript),
71 /// Sends notes holding assets the non-fungible faucet mints as part of note creation.
72 NonFungible(SendNonFungibleFaucetNotesTransactionScript),
73}
74
75impl SendNotesTransactionScript {
76 // CONSTANTS
77 // --------------------------------------------------------------------------------------------
78
79 /// Number of elements in the payload header word: `[num_notes, expiration_delta, 0, 0]`.
80 ///
81 /// See `encode_payload` for the full payload layout.
82 pub const PAYLOAD_HEADER_NUM_ELEMENTS: usize = 4;
83
84 /// Element offset of the asset count within a note record, after the RECIPIENT word and the
85 /// `tag` and `note_type` elements.
86 ///
87 /// See `encode_payload` for the full payload layout.
88 pub const NOTE_RECORD_NUM_ASSETS_OFFSET: usize = 6;
89
90 /// Element offset of the first asset or attachment item within a note record, after the
91 /// RECIPIENT word and the metadata word.
92 ///
93 /// See `encode_payload` for the full payload layout.
94 pub const NOTE_RECORD_ITEMS_OFFSET: usize = 8;
95
96 /// Number of elements a single asset or attachment item occupies (two words).
97 ///
98 /// See `encode_payload` for the full payload layout.
99 pub const ITEM_NUM_ELEMENTS: usize = 8;
100
101 // CONSTRUCTORS
102 // --------------------------------------------------------------------------------------------
103
104 /// Builds the `send_notes` script that the account described by `interface` calls for, without
105 /// an expiration delta.
106 ///
107 /// See [`Self::with_expiration_delta`] for the variant that pins the transaction to a
108 /// reference-block delta.
109 ///
110 /// # Errors
111 ///
112 /// Returns an error if the interface exposes none of the wallet, fungible faucet or
113 /// non-fungible faucet procedures, or if the notes fail the selected script's validation.
114 pub fn new(
115 interface: &AccountCodeInterface,
116 output_notes: &[PartialNote],
117 ) -> Result<Self, SendNotesTransactionScriptError> {
118 Self::build(interface, output_notes, 0)
119 }
120
121 /// Builds the `send_notes` script that the account described by `interface` calls for, with the
122 /// given non-zero expiration delta.
123 ///
124 /// The delta specifies how close to the transaction's reference block the transaction must be
125 /// included into the chain. For example, with a reference block of 100 and a delta of 10, the
126 /// transaction must be included by block 110.
127 ///
128 /// # Errors
129 ///
130 /// See [`Self::new`].
131 pub fn with_expiration_delta(
132 interface: &AccountCodeInterface,
133 output_notes: &[PartialNote],
134 expiration_delta: NonZeroU16,
135 ) -> Result<Self, SendNotesTransactionScriptError> {
136 Self::build(interface, output_notes, expiration_delta.get())
137 }
138
139 // PUBLIC ACCESSORS
140 // --------------------------------------------------------------------------------------------
141
142 /// The underlying [`TransactionScript`], to be set as the transaction's script.
143 pub fn tx_script(&self) -> &TransactionScript {
144 match self {
145 Self::Wallet(script) => script.tx_script(),
146 Self::Fungible(script) => script.tx_script(),
147 Self::NonFungible(script) => script.tx_script(),
148 }
149 }
150
151 /// The transaction script argument the script reads its payload commitment from.
152 ///
153 /// Pass this as the transaction's `TX_SCRIPT_ARGS`.
154 pub fn tx_script_args(&self) -> Word {
155 match self {
156 Self::Wallet(script) => script.tx_script_args(),
157 Self::Fungible(script) => script.tx_script_args(),
158 Self::NonFungible(script) => script.tx_script_args(),
159 }
160 }
161
162 /// The [`TransactionScriptRoot`]s of every canonical `send_notes` script.
163 ///
164 /// Allowlisting all of them covers any account this type can build a script for.
165 pub fn script_roots() -> [TransactionScriptRoot; 3] {
166 [
167 SendWalletNotesTransactionScript::script_root(),
168 SendFungibleFaucetNotesTransactionScript::script_root(),
169 SendNonFungibleFaucetNotesTransactionScript::script_root(),
170 ]
171 }
172
173 // HELPER FUNCTIONS
174 // --------------------------------------------------------------------------------------------
175
176 fn build(
177 interface: &AccountCodeInterface,
178 output_notes: &[PartialNote],
179 expiration_delta: u16,
180 ) -> Result<Self, SendNotesTransactionScriptError> {
181 let fungible_faucet = interface.contains([FungibleFaucet::mint_and_send_root()]);
182 let non_fungible_faucet = interface.contains([NonFungibleFaucet::mint_and_send_root()]);
183 let basic_wallet = interface
184 .contains([BasicWallet::move_asset_to_note_root(), BasicWallet::create_note_root()]);
185
186 // A faucet script mints one asset per note as part of note creation, so it applies only to
187 // notes shaped that way whose assets all have the composition that faucet mints. Notes the
188 // faucet cannot mint hold assets the account already owns, which the wallet script sends.
189 let one_asset_per_note = output_notes.iter().all(|note| note.assets().num_assets() == 1);
190 let mints_composition = |composition| {
191 one_asset_per_note
192 && output_notes
193 .iter()
194 .flat_map(|note| note.assets().iter())
195 .all(|asset| asset.id().composition() == composition)
196 };
197
198 if fungible_faucet && mints_composition(AssetComposition::Fungible) {
199 SendFungibleFaucetNotesTransactionScript::build(
200 interface,
201 output_notes,
202 expiration_delta,
203 )
204 .map(Self::Fungible)
205 } else if non_fungible_faucet && mints_composition(AssetComposition::None) {
206 SendNonFungibleFaucetNotesTransactionScript::build(
207 interface,
208 output_notes,
209 expiration_delta,
210 )
211 .map(Self::NonFungible)
212 } else if basic_wallet {
213 SendWalletNotesTransactionScript::build(interface, output_notes, expiration_delta)
214 .map(Self::Wallet)
215 } else if !(fungible_faucet || non_fungible_faucet) {
216 Err(SendNotesTransactionScriptError::UnsupportedAccountInterface)
217 } else if !one_asset_per_note {
218 Err(SendNotesTransactionScriptError::FaucetNoteUnexpectedNumAssets)
219 } else {
220 // The notes are shaped for a faucet, but carry an asset composition this one does not
221 // mint.
222 let expected = if fungible_faucet {
223 AssetComposition::Fungible
224 } else {
225 AssetComposition::None
226 };
227 let actual = output_notes
228 .iter()
229 .flat_map(|note| note.assets().iter())
230 .map(|asset| asset.id().composition())
231 .find(|composition| *composition != expected)
232 .unwrap_or(expected);
233
234 Err(SendNotesTransactionScriptError::AssetCompositionMismatch { expected, actual })
235 }
236 }
237}
238
239// SEND WALLET NOTES TRANSACTION SCRIPT
240// ================================================================================================
241
242/// The canonical `send_notes` [`TransactionScript`] for accounts exposing the [`BasicWallet`]
243/// procedures, which sends notes holding assets the account already owns.
244///
245/// The payload and the attachment contents the script reads from the advice provider are embedded
246/// in the script's MAST forest, so they are loaded with the script. Callers only have to set the
247/// script ([`Self::tx_script`]) and the payload commitment it reads its parameters from
248/// ([`Self::tx_script_args`]).
249#[derive(Debug, Clone)]
250pub struct SendWalletNotesTransactionScript(SendNotesScript);
251
252impl SendWalletNotesTransactionScript {
253 /// Builds the script for the account described by `interface`, without an expiration delta.
254 ///
255 /// # Errors
256 ///
257 /// See [`Self::with_expiration_delta`].
258 pub fn new(
259 interface: &AccountCodeInterface,
260 output_notes: &[PartialNote],
261 ) -> Result<Self, SendNotesTransactionScriptError> {
262 Self::build(interface, output_notes, 0)
263 }
264
265 /// Builds the script for the account described by `interface`, with the given non-zero
266 /// expiration delta.
267 ///
268 /// # Errors
269 ///
270 /// Returns an error if:
271 /// - The interface does not expose the [`BasicWallet`] procedures the script calls.
272 /// - Any note is not sent by the account.
273 pub fn with_expiration_delta(
274 interface: &AccountCodeInterface,
275 output_notes: &[PartialNote],
276 expiration_delta: NonZeroU16,
277 ) -> Result<Self, SendNotesTransactionScriptError> {
278 Self::build(interface, output_notes, expiration_delta.get())
279 }
280
281 /// The underlying [`TransactionScript`], to be set as the transaction's script.
282 pub fn tx_script(&self) -> &TransactionScript {
283 self.0.tx_script()
284 }
285
286 /// The transaction script argument the script reads its payload commitment from.
287 pub fn tx_script_args(&self) -> Word {
288 self.0.tx_script_args()
289 }
290
291 /// The [`TransactionScriptRoot`] of the canonical wallet script.
292 pub fn script_root() -> TransactionScriptRoot {
293 SEND_NOTES_WALLET_TX_SCRIPT.root()
294 }
295
296 fn build(
297 interface: &AccountCodeInterface,
298 output_notes: &[PartialNote],
299 expiration_delta: u16,
300 ) -> Result<Self, SendNotesTransactionScriptError> {
301 // The script calls both procedures, so both must be exposed.
302 let can_send_own_assets = interface
303 .contains([BasicWallet::move_asset_to_note_root(), BasicWallet::create_note_root()]);
304 if !can_send_own_assets {
305 return Err(SendNotesTransactionScriptError::UnsupportedAccountInterface);
306 }
307
308 for note in output_notes {
309 validate_note_sender(interface.id(), note)?;
310 }
311
312 Ok(Self(SendNotesScript::new(
313 SEND_NOTES_WALLET_TX_SCRIPT.clone(),
314 output_notes,
315 expiration_delta,
316 )))
317 }
318}
319
320// SEND FUNGIBLE FAUCET NOTES TRANSACTION SCRIPT
321// ================================================================================================
322
323/// The canonical `send_notes` [`TransactionScript`] for accounts exposing the [`FungibleFaucet`]
324/// procedures, which sends notes holding assets the faucet mints as part of note creation.
325///
326/// Every note must carry exactly one fungible asset, issued by this faucet.
327///
328/// Faucets that delegate minting to an authority (those exposing [`Ownable2Step`] or
329/// [`RoleBasedAccessControl`]) are network faucets that mint exclusively via MINT notes, so they
330/// are rejected at script build time to avoid runtime failures under their OwnerOnly mint policy.
331///
332/// The payload and the attachment contents the script reads from the advice provider are embedded
333/// in the script's MAST forest, so they are loaded with the script. Callers only have to set the
334/// script ([`Self::tx_script`]) and the payload commitment it reads its parameters from
335/// ([`Self::tx_script_args`]).
336#[derive(Debug, Clone)]
337pub struct SendFungibleFaucetNotesTransactionScript(SendNotesScript);
338
339impl SendFungibleFaucetNotesTransactionScript {
340 /// Builds the script for the faucet described by `interface`, without an expiration delta.
341 ///
342 /// # Errors
343 ///
344 /// See [`Self::with_expiration_delta`].
345 pub fn new(
346 interface: &AccountCodeInterface,
347 output_notes: &[PartialNote],
348 ) -> Result<Self, SendNotesTransactionScriptError> {
349 Self::build(interface, output_notes, 0)
350 }
351
352 /// Builds the script for the faucet described by `interface`, with the given non-zero
353 /// expiration delta.
354 ///
355 /// # Errors
356 ///
357 /// Returns an error if:
358 /// - The interface does not expose the [`FungibleFaucet`] procedure the script calls.
359 /// - The faucet delegates minting to an authority.
360 /// - Any note is not sent by the faucet.
361 /// - Any note does not carry exactly one fungible asset issued by the faucet.
362 pub fn with_expiration_delta(
363 interface: &AccountCodeInterface,
364 output_notes: &[PartialNote],
365 expiration_delta: NonZeroU16,
366 ) -> Result<Self, SendNotesTransactionScriptError> {
367 Self::build(interface, output_notes, expiration_delta.get())
368 }
369
370 /// The underlying [`TransactionScript`], to be set as the transaction's script.
371 pub fn tx_script(&self) -> &TransactionScript {
372 self.0.tx_script()
373 }
374
375 /// The transaction script argument the script reads its payload commitment from.
376 pub fn tx_script_args(&self) -> Word {
377 self.0.tx_script_args()
378 }
379
380 /// The [`TransactionScriptRoot`] of the canonical fungible faucet script.
381 pub fn script_root() -> TransactionScriptRoot {
382 SEND_NOTES_FUNGIBLE_FAUCET_TX_SCRIPT.root()
383 }
384
385 fn build(
386 interface: &AccountCodeInterface,
387 output_notes: &[PartialNote],
388 expiration_delta: u16,
389 ) -> Result<Self, SendNotesTransactionScriptError> {
390 validate_faucet(interface, FungibleFaucet::mint_and_send_root())?;
391 validate_minted_notes(interface.id(), output_notes, AssetComposition::Fungible)?;
392
393 Ok(Self(SendNotesScript::new(
394 SEND_NOTES_FUNGIBLE_FAUCET_TX_SCRIPT.clone(),
395 output_notes,
396 expiration_delta,
397 )))
398 }
399}
400
401// SEND NON-FUNGIBLE FAUCET NOTES TRANSACTION SCRIPT
402// ================================================================================================
403
404/// The canonical `send_notes` [`TransactionScript`] for accounts exposing the [`NonFungibleFaucet`]
405/// procedures, which sends notes holding assets the faucet mints as part of note creation.
406///
407/// Every note must carry exactly one non-fungible asset, issued by this faucet. Unlike the fungible
408/// faucet, `non_fungible::mint_and_send` derives the asset from the faucet itself, so the script
409/// passes it only the asset's commitment.
410///
411/// Faucets that delegate minting to an authority (those exposing [`Ownable2Step`] or
412/// [`RoleBasedAccessControl`]) are network faucets that mint exclusively via MINT notes, so they
413/// are rejected at script build time to avoid runtime failures under their OwnerOnly mint policy.
414///
415/// The payload and the attachment contents the script reads from the advice provider are embedded
416/// in the script's MAST forest, so they are loaded with the script. Callers only have to set the
417/// script ([`Self::tx_script`]) and the payload commitment it reads its parameters from
418/// ([`Self::tx_script_args`]).
419#[derive(Debug, Clone)]
420pub struct SendNonFungibleFaucetNotesTransactionScript(SendNotesScript);
421
422impl SendNonFungibleFaucetNotesTransactionScript {
423 /// Builds the script for the faucet described by `interface`, without an expiration delta.
424 ///
425 /// # Errors
426 ///
427 /// See [`Self::with_expiration_delta`].
428 pub fn new(
429 interface: &AccountCodeInterface,
430 output_notes: &[PartialNote],
431 ) -> Result<Self, SendNotesTransactionScriptError> {
432 Self::build(interface, output_notes, 0)
433 }
434
435 /// Builds the script for the faucet described by `interface`, with the given non-zero
436 /// expiration delta.
437 ///
438 /// # Errors
439 ///
440 /// Returns an error if:
441 /// - The interface does not expose the [`NonFungibleFaucet`] procedure the script calls.
442 /// - The faucet delegates minting to an authority.
443 /// - Any note is not sent by the faucet.
444 /// - Any note does not carry exactly one non-fungible asset issued by the faucet.
445 pub fn with_expiration_delta(
446 interface: &AccountCodeInterface,
447 output_notes: &[PartialNote],
448 expiration_delta: NonZeroU16,
449 ) -> Result<Self, SendNotesTransactionScriptError> {
450 Self::build(interface, output_notes, expiration_delta.get())
451 }
452
453 /// The underlying [`TransactionScript`], to be set as the transaction's script.
454 pub fn tx_script(&self) -> &TransactionScript {
455 self.0.tx_script()
456 }
457
458 /// The transaction script argument the script reads its payload commitment from.
459 pub fn tx_script_args(&self) -> Word {
460 self.0.tx_script_args()
461 }
462
463 /// The [`TransactionScriptRoot`] of the canonical non-fungible faucet script.
464 pub fn script_root() -> TransactionScriptRoot {
465 SEND_NOTES_NON_FUNGIBLE_FAUCET_TX_SCRIPT.root()
466 }
467
468 fn build(
469 interface: &AccountCodeInterface,
470 output_notes: &[PartialNote],
471 expiration_delta: u16,
472 ) -> Result<Self, SendNotesTransactionScriptError> {
473 validate_faucet(interface, NonFungibleFaucet::mint_and_send_root())?;
474 validate_minted_notes(interface.id(), output_notes, AssetComposition::None)?;
475
476 Ok(Self(SendNotesScript::new(
477 SEND_NOTES_NON_FUNGIBLE_FAUCET_TX_SCRIPT.clone(),
478 output_notes,
479 expiration_delta,
480 )))
481 }
482}
483
484// SEND NOTES SCRIPT
485// ================================================================================================
486
487/// The state every `send_notes` script shares: the script itself, with the data it reads from the
488/// advice provider embedded in its MAST forest, and the commitment to that data.
489#[derive(Debug, Clone)]
490struct SendNotesScript {
491 script: TransactionScript,
492 tx_script_args: Word,
493}
494
495impl SendNotesScript {
496 /// Encodes `output_notes` into the payload the `send_notes` scripts expect and embeds it, along
497 /// with every attachment's contents, into `script`'s MAST forest.
498 fn new(script: TransactionScript, output_notes: &[PartialNote], expiration_delta: u16) -> Self {
499 let payload = encode_payload(output_notes, expiration_delta);
500 let tx_script_args = Hasher::hash_elements(&payload);
501
502 // Embed the data the script reads from the advice provider into the script's MAST forest,
503 // so it is loaded automatically and callers only have to set the script and its arguments.
504 let mut advice_map = AdviceMap::default();
505 advice_map.insert(tx_script_args, payload);
506 for note in output_notes {
507 for attachment in note.attachments().iter() {
508 advice_map.insert(attachment.to_commitment(), attachment.to_elements());
509 }
510 }
511
512 Self {
513 script: script.with_advice_map(advice_map),
514 tx_script_args,
515 }
516 }
517
518 fn tx_script(&self) -> &TransactionScript {
519 &self.script
520 }
521
522 fn tx_script_args(&self) -> Word {
523 self.tx_script_args
524 }
525}
526
527// SEND NOTES SCRIPT ERROR
528// ================================================================================================
529
530/// Errors that can occur while building a [`SendNotesTransactionScript`].
531#[derive(Debug, Error)]
532pub enum SendNotesTransactionScriptError {
533 #[error("note asset is not issued by faucet {0}")]
534 IssuanceFaucetMismatch(AccountId),
535 #[error("note created by the faucet doesn't contain exactly one asset")]
536 FaucetNoteUnexpectedNumAssets,
537 #[error(
538 "note asset has the {actual} composition but the faucet mints assets with the {expected} \
539 composition"
540 )]
541 AssetCompositionMismatch {
542 expected: AssetComposition,
543 actual: AssetComposition,
544 },
545 #[error("invalid sender account: {0}")]
546 InvalidSenderAccount(AccountId),
547 #[error(
548 "account does not contain the basic wallet, fungible faucet or non-fungible faucet \
549 interfaces which are needed to support the send_notes script generation"
550 )]
551 UnsupportedAccountInterface,
552}
553
554// HELPER FUNCTIONS
555// ================================================================================================
556
557/// Validates that `interface` exposes `mint_and_send_root` and does not delegate minting to an
558/// authority.
559///
560/// A faucet that delegates minting is a network faucet: it mints exclusively via MINT notes, so the
561/// standard `send_notes` flow would fail at runtime under its OwnerOnly mint policy. Exposing
562/// either access-control component signals this.
563fn validate_faucet(
564 interface: &AccountCodeInterface,
565 mint_and_send_root: AccountProcedureRoot,
566) -> Result<(), SendNotesTransactionScriptError> {
567 if !interface.contains([mint_and_send_root]) {
568 return Err(SendNotesTransactionScriptError::UnsupportedAccountInterface);
569 }
570
571 let is_authority_controlled = interface.contains(Ownable2Step::code().procedure_roots())
572 || interface.contains(RoleBasedAccessControl::code().procedure_roots());
573 if is_authority_controlled {
574 return Err(SendNotesTransactionScriptError::UnsupportedAccountInterface);
575 }
576
577 Ok(())
578}
579
580/// Validates that every note is sent by `sender` and carries exactly one asset issued by it with
581/// the `expected_composition`, as both faucet scripts mint exactly one asset of the composition
582/// their faucet type defines per note.
583fn validate_minted_notes(
584 sender: AccountId,
585 notes: &[PartialNote],
586 expected_composition: AssetComposition,
587) -> Result<(), SendNotesTransactionScriptError> {
588 for note in notes {
589 validate_note_sender(sender, note)?;
590
591 if note.assets().num_assets() != 1 {
592 return Err(SendNotesTransactionScriptError::FaucetNoteUnexpectedNumAssets);
593 }
594 let asset = note.assets().iter().next().expect("note should contain an asset");
595 if asset.faucet_id() != sender {
596 return Err(SendNotesTransactionScriptError::IssuanceFaucetMismatch(asset.faucet_id()));
597 }
598
599 let composition = asset.id().composition();
600 if composition != expected_composition {
601 return Err(SendNotesTransactionScriptError::AssetCompositionMismatch {
602 expected: expected_composition,
603 actual: composition,
604 });
605 }
606 }
607 Ok(())
608}
609
610/// Validates that `note` is sent by `sender`.
611fn validate_note_sender(
612 sender: AccountId,
613 note: &PartialNote,
614) -> Result<(), SendNotesTransactionScriptError> {
615 if note.metadata().sender() != sender {
616 return Err(SendNotesTransactionScriptError::InvalidSenderAccount(
617 note.metadata().sender(),
618 ));
619 }
620 Ok(())
621}
622
623/// Encodes the notes and expiration delta into the payload element expected by the `send_notes`
624/// MASM scripts. The payload structure is as follows:
625/// ```text
626/// word 0 (header): [num_notes, expiration_delta, 0, 0]
627/// per note record:
628/// word 0: RECIPIENT
629/// word 1: [tag, note_type, num_assets, num_attachments]
630/// num_assets * 2 words: ASSET_ID, ASSET_VALUE
631/// num_attachments * 2 words: [attachment_scheme, 0, 0, 0], ATTACHMENT_COMMITMENT
632/// ```
633fn encode_payload(notes: &[PartialNote], expiration_delta: u16) -> Vec<Felt> {
634 // SAFETY: kernel caps output notes and assets per note below u32::MAX, so these conversions
635 // cannot truncate for any executable transaction.
636 let num_notes = u32::try_from(notes.len()).expect("note count should fit in a u32");
637
638 let mut payload = alloc::vec![Felt::from(num_notes), Felt::from(expiration_delta), ZERO, ZERO];
639 debug_assert_eq!(
640 payload.len(),
641 SendNotesTransactionScript::PAYLOAD_HEADER_NUM_ELEMENTS,
642 "header size should match the advertised constant"
643 );
644
645 for note in notes {
646 let num_assets =
647 u32::try_from(note.assets().num_assets()).expect("asset count should fit in a u32");
648
649 let record_start = payload.len();
650 payload.extend(note.recipient_digest().iter());
651 payload.push(Felt::from(note.metadata().tag()));
652 payload.push(Felt::from(note.metadata().note_type()));
653 debug_assert_eq!(
654 payload.len() - record_start,
655 SendNotesTransactionScript::NOTE_RECORD_NUM_ASSETS_OFFSET,
656 "asset count should sit at the advertised record offset"
657 );
658 payload.push(Felt::from(num_assets));
659 payload.push(Felt::from(note.attachments().num_attachments()));
660 debug_assert_eq!(
661 payload.len() - record_start,
662 SendNotesTransactionScript::NOTE_RECORD_ITEMS_OFFSET,
663 "items should start at the advertised record offset"
664 );
665
666 for asset in note.assets().iter() {
667 let item_start = payload.len();
668 payload.extend(asset.to_id_word().iter());
669 payload.extend(asset.to_value_word().iter());
670 debug_assert_eq!(
671 payload.len() - item_start,
672 SendNotesTransactionScript::ITEM_NUM_ELEMENTS,
673 "an asset item should occupy the advertised number of elements"
674 );
675 }
676
677 for attachment in note.attachments().iter() {
678 payload.push(Felt::from(attachment.attachment_scheme().as_u16()));
679 payload.extend([ZERO; 3]);
680 payload.extend(attachment.to_commitment().iter());
681 }
682 }
683
684 payload
685}