1use alloc::boxed::Box;
4use alloc::collections::{BTreeMap, BTreeSet};
5use alloc::string::{String, ToString};
6use alloc::vec::Vec;
7use core::num::NonZeroU16;
8
9use miden_protocol::account::{AccountCodeInterface, AccountCodeUpgrade, AccountId};
10use miden_protocol::asset::Asset;
11use miden_protocol::block::BlockNumber;
12use miden_protocol::crypto::SequentialCommit;
13use miden_protocol::crypto::merkle::MerkleError;
14use miden_protocol::crypto::merkle::store::MerkleStore;
15use miden_protocol::errors::{
16 AccountError,
17 AssetError,
18 NoteError,
19 StorageMapError,
20 TransactionInputError,
21};
22use miden_protocol::note::{
23 Note,
24 NoteDetails,
25 NoteDetailsCommitment,
26 NoteId,
27 NoteRecipient,
28 NoteScript,
29 NoteTag,
30 PartialNote,
31};
32use miden_protocol::transaction::{
33 InputNote,
34 InputNotes,
35 TransactionArgs,
36 TransactionId,
37 TransactionScript,
38};
39use miden_protocol::vm::AdviceMap;
40use miden_protocol::{Felt, Word};
41use miden_standards::errors::CodeBuilderError;
42use miden_standards::tx_script::{
43 ExpirationTransactionScript,
44 SendNotesTransactionScript,
45 SendNotesTransactionScriptError,
46};
47use miden_tx::utils::serde::{
48 ByteReader,
49 ByteWriter,
50 Deserializable,
51 DeserializationError,
52 Serializable,
53};
54use thiserror::Error;
55
56mod builder;
57mod code_upgrade;
58pub use builder::{
59 PaymentNoteDescription,
60 PswapTransactionData,
61 SwapTransactionData,
62 TransactionRequestBuilder,
63};
64
65mod foreign;
66pub(crate) use foreign::account_proof_into_inputs;
67pub use foreign::{ForeignAccount, build_fpi_script};
68
69use crate::store::InputNoteRecord;
70
71pub type NoteArgs = Word;
75
76#[derive(Clone, Debug, PartialEq, Eq)]
81pub enum TransactionScriptTemplate {
82 CustomScript(TransactionScript),
84 SendNotes(Vec<PartialNote>),
90}
91
92#[derive(Clone, Debug, PartialEq, Eq)]
98pub struct TransactionRequest {
99 block_numbers: BTreeSet<BlockNumber>,
101 input_notes: Vec<Note>,
106 input_notes_args: Vec<(NoteId, Option<NoteArgs>)>,
109 pub(super) explicit_input_notes: BTreeMap<NoteId, InputNote>,
111 script_template: Option<TransactionScriptTemplate>,
113 expected_output_recipients: BTreeMap<Word, NoteRecipient>,
115 expected_future_notes: BTreeMap<NoteDetailsCommitment, (NoteDetails, NoteTag)>,
120 advice_map: AdviceMap,
122 merkle_store: MerkleStore,
124 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
128 expiration_delta: Option<u16>,
131 ignore_invalid_input_notes: bool,
134 script_arg: Option<Word>,
137 auth_arg: Option<Word>,
140 fee_conversion_salt: Option<Word>,
144 expected_ntx_scripts: Vec<NoteScript>,
148 account_code_upgrade: Option<AccountCodeUpgrade>,
151}
152
153impl TransactionRequest {
154 pub fn block_numbers(&self) -> &BTreeSet<BlockNumber> {
159 &self.block_numbers
160 }
161
162 pub fn input_notes(&self) -> &[Note] {
164 &self.input_notes
165 }
166
167 pub fn input_note_ids(&self) -> impl Iterator<Item = NoteId> {
169 self.input_notes.iter().map(Note::id)
170 }
171
172 pub fn incoming_assets(&self) -> (BTreeMap<AccountId, u64>, Vec<Asset>) {
174 collect_assets(self.input_notes.iter().flat_map(|note| note.assets().iter()))
175 }
176
177 pub fn get_note_args(&self) -> BTreeMap<NoteId, NoteArgs> {
180 self.input_notes_args
181 .iter()
182 .filter_map(|(note, args)| args.map(|a| (*note, a)))
183 .collect()
184 }
185
186 pub fn expected_output_own_notes(&self) -> Vec<Note> {
191 match &self.script_template {
192 Some(TransactionScriptTemplate::SendNotes(notes)) => notes
193 .iter()
194 .map(|partial| {
195 Note::with_attachments(
196 partial.assets().clone(),
197 *partial.partial_metadata(),
198 self.expected_output_recipients
199 .get(&partial.recipient_digest())
200 .expect("Recipient should be included if it's an own note")
201 .clone(),
202 partial.attachments().clone(),
203 )
204 })
205 .collect(),
206 _ => vec![],
207 }
208 }
209
210 pub fn expected_output_recipients(&self) -> impl Iterator<Item = &NoteRecipient> {
212 self.expected_output_recipients.values()
213 }
214
215 pub fn expected_future_notes(&self) -> impl Iterator<Item = &(NoteDetails, NoteTag)> {
217 self.expected_future_notes.values()
218 }
219
220 pub fn script_template(&self) -> &Option<TransactionScriptTemplate> {
222 &self.script_template
223 }
224
225 pub fn advice_map(&self) -> &AdviceMap {
227 &self.advice_map
228 }
229
230 pub fn advice_map_mut(&mut self) -> &mut AdviceMap {
232 &mut self.advice_map
233 }
234
235 pub fn merkle_store(&self) -> &MerkleStore {
237 &self.merkle_store
238 }
239
240 pub fn foreign_accounts(&self) -> &BTreeMap<AccountId, ForeignAccount> {
242 &self.foreign_accounts
243 }
244
245 pub fn ignore_invalid_input_notes(&self) -> bool {
247 self.ignore_invalid_input_notes
248 }
249
250 pub fn script_arg(&self) -> &Option<Word> {
252 &self.script_arg
253 }
254
255 pub fn auth_arg(&self) -> &Option<Word> {
257 &self.auth_arg
258 }
259
260 pub fn fee_conversion_salt(&self) -> Option<Word> {
263 self.fee_conversion_salt
264 }
265
266 pub fn has_auth_arg(&self) -> bool {
271 self.auth_arg.is_some_and(|auth_arg| auth_arg != Word::empty())
272 }
273
274 pub fn expected_ntx_scripts(&self) -> &[NoteScript] {
276 &self.expected_ntx_scripts
277 }
278
279 pub fn account_code_upgrade(&self) -> Option<&AccountCodeUpgrade> {
281 self.account_code_upgrade.as_ref()
282 }
283
284 #[cfg(feature = "testing")]
292 pub(crate) fn add_unauthenticated_input_note(&mut self, note: Note) {
293 self.input_notes_args.push((note.id(), None));
294 self.input_notes.push(note);
295 }
296
297 #[must_use]
305 pub fn with_auth_args<A: SequentialCommit<Commitment = Word>>(mut self, auth_args: &A) -> Self {
306 self.commit_auth_args(auth_args.to_commitment(), auth_args.to_elements());
307 self.fee_conversion_salt = None;
308 self
309 }
310
311 pub(crate) fn commit_auth_args(&mut self, auth_arg: Word, preimage: Vec<Felt>) {
313 self.advice_map.insert(auth_arg, preimage);
314 self.auth_arg = Some(auth_arg);
315 }
316
317 fn validate(&self) -> Result<(), TransactionRequestError> {
322 let mut seen_input_notes = BTreeSet::new();
323 for (note_id, _) in &self.input_notes_args {
324 if !seen_input_notes.insert(note_id) {
325 return Err(TransactionRequestError::DuplicateInputNote(*note_id));
326 }
327 }
328
329 Ok(())
330 }
331
332 pub(crate) fn build_input_notes(
338 &self,
339 authenticated_note_records: Vec<InputNoteRecord>,
340 ) -> Result<InputNotes<InputNote>, TransactionRequestError> {
341 let mut authenticated_notes: BTreeMap<NoteId, InputNoteRecord> = BTreeMap::new();
342 for record in authenticated_note_records {
343 let note_id =
346 record.id().expect("authenticated note record carries metadata so id() is Some");
347
348 if !record.is_authenticated() {
349 return Err(TransactionRequestError::InputNoteNotAuthenticated(note_id));
350 }
351 if record.is_consumed() {
352 return Err(TransactionRequestError::InputNoteAlreadyConsumed(
353 record.details_commitment(),
354 ));
355 }
356
357 authenticated_notes.insert(note_id, record);
358 }
359
360 let input_notes = self
361 .input_notes()
362 .iter()
363 .map(|note| match self.explicit_input_notes.get(¬e.id()) {
364 Some(input_note) => input_note.clone(),
365 None => match authenticated_notes.remove(¬e.id()) {
366 Some(record) => record
367 .try_into()
368 .expect("Authenticated note record should be convertible to InputNote"),
369 None => InputNote::unauthenticated(note.clone()),
370 },
371 })
372 .collect();
373
374 Ok(InputNotes::new(input_notes)?)
375 }
376
377 pub(crate) fn into_transaction_args(
380 self,
381 tx_script: Option<(TransactionScript, Option<Word>)>,
382 ) -> TransactionArgs {
383 let note_args = self.get_note_args();
384 let TransactionRequest {
385 expected_output_recipients,
386 advice_map,
387 merkle_store,
388 account_code_upgrade,
389 ..
390 } = self;
391
392 let mut tx_args = TransactionArgs::new(advice_map).with_note_args(note_args);
393
394 if let Some((tx_script, script_args)) = tx_script {
398 let script_args = script_args.or(self.script_arg).unwrap_or_default();
399 tx_args = tx_args.with_tx_script_and_args(tx_script, script_args);
400 }
401
402 if let Some(auth_argument) = self.auth_arg {
403 tx_args = tx_args.with_auth_args(auth_argument);
404 }
405
406 if let Some(account_code_upgrade) = account_code_upgrade {
407 tx_args = tx_args.with_account_code_upgrade(account_code_upgrade);
408 }
409
410 tx_args
411 .extend_output_note_recipients(expected_output_recipients.into_values().map(Box::new));
412 tx_args.extend_merkle_store(merkle_store.inner_nodes());
413
414 tx_args
415 }
416
417 pub(crate) fn build_transaction_script(
435 &self,
436 code_interface: &AccountCodeInterface,
437 ) -> Result<Option<(TransactionScript, Option<Word>)>, TransactionRequestError> {
438 match &self.script_template {
439 Some(TransactionScriptTemplate::CustomScript(script)) => {
440 Ok(Some((script.clone(), None)))
441 },
442 Some(TransactionScriptTemplate::SendNotes(notes)) => {
443 let script = match self.expiration_delta.and_then(NonZeroU16::new) {
444 Some(delta) => SendNotesTransactionScript::with_expiration_delta(
445 code_interface,
446 notes,
447 delta,
448 )?,
449 None => SendNotesTransactionScript::new(code_interface, notes)?,
450 };
451 Ok(Some((script.tx_script().clone(), Some(script.tx_script_args()))))
452 },
453 None => match self.expiration_delta.and_then(NonZeroU16::new) {
454 Some(delta) => {
455 let script = ExpirationTransactionScript::new(delta);
456 Ok(Some((script.into(), Some(script.tx_script_args()))))
457 },
458 None => Ok(None),
459 },
460 }
461 }
462}
463
464impl Serializable for TransactionRequest {
468 fn write_into<W: ByteWriter>(&self, target: &mut W) {
469 self.block_numbers.write_into(target);
470 self.input_notes.write_into(target);
471 self.input_notes_args.write_into(target);
472 self.explicit_input_notes.write_into(target);
473 match &self.script_template {
474 None => target.write_u8(0),
475 Some(TransactionScriptTemplate::CustomScript(script)) => {
476 target.write_u8(1);
477 script.write_into(target);
478 },
479 Some(TransactionScriptTemplate::SendNotes(notes)) => {
480 target.write_u8(2);
481 notes.write_into(target);
482 },
483 }
484 self.expected_output_recipients.write_into(target);
485 self.expected_future_notes.write_into(target);
486 self.advice_map.write_into(target);
487 self.merkle_store.write_into(target);
488 let foreign_accounts: Vec<_> = self.foreign_accounts.values().cloned().collect();
489 foreign_accounts.write_into(target);
490 self.expiration_delta.write_into(target);
491 target.write_u8(u8::from(self.ignore_invalid_input_notes));
492 self.script_arg.write_into(target);
493 self.auth_arg.write_into(target);
494 self.fee_conversion_salt.write_into(target);
495 self.expected_ntx_scripts.write_into(target);
496 self.account_code_upgrade.write_into(target);
497 }
498}
499
500impl Deserializable for TransactionRequest {
501 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
502 let block_numbers = BTreeSet::<BlockNumber>::read_from(source)?;
503 let input_notes = Vec::<Note>::read_from(source)?;
504 let input_notes_args = Vec::<(NoteId, Option<NoteArgs>)>::read_from(source)?;
505 let explicit_input_notes = BTreeMap::<NoteId, InputNote>::read_from(source)?;
506 for (note_id, input_note) in &explicit_input_notes {
507 if *note_id != input_note.id() || !input_notes.contains(input_note.note()) {
508 return Err(DeserializationError::InvalidValue(format!(
509 "explicit input note {note_id} does not match a request input note"
510 )));
511 }
512 }
513
514 let script_template = match source.read_u8()? {
515 0 => None,
516 1 => {
517 let transaction_script = TransactionScript::read_from(source)?;
518 Some(TransactionScriptTemplate::CustomScript(transaction_script))
519 },
520 2 => {
521 let notes = Vec::<PartialNote>::read_from(source)?;
522 Some(TransactionScriptTemplate::SendNotes(notes))
523 },
524 _ => {
525 return Err(DeserializationError::InvalidValue(
526 "Invalid script template type".to_string(),
527 ));
528 },
529 };
530
531 let expected_output_recipients = BTreeMap::<Word, NoteRecipient>::read_from(source)?;
532 let expected_future_notes =
533 BTreeMap::<NoteDetailsCommitment, (NoteDetails, NoteTag)>::read_from(source)?;
534
535 let advice_map = AdviceMap::read_from(source)?;
536 let merkle_store = MerkleStore::read_from(source)?;
537 let mut foreign_accounts = BTreeMap::new();
538 for foreign_account in Vec::<ForeignAccount>::read_from(source)? {
539 foreign_accounts.entry(foreign_account.account_id()).or_insert(foreign_account);
540 }
541 let expiration_delta = Option::<u16>::read_from(source)?;
542 let ignore_invalid_input_notes = source.read_u8()? == 1;
543 let script_arg = Option::<Word>::read_from(source)?;
544 let auth_arg = Option::<Word>::read_from(source)?;
545 let fee_conversion_salt = Option::<Word>::read_from(source)?;
546 let expected_ntx_scripts = Vec::<NoteScript>::read_from(source)?;
547 let account_code_upgrade = Option::<AccountCodeUpgrade>::read_from(source)?;
548
549 let request = TransactionRequest {
550 block_numbers,
551 input_notes,
552 input_notes_args,
553 explicit_input_notes,
554 script_template,
555 expected_output_recipients,
556 expected_future_notes,
557 advice_map,
558 merkle_store,
559 foreign_accounts,
560 expiration_delta,
561 ignore_invalid_input_notes,
562 script_arg,
563 auth_arg,
564 fee_conversion_salt,
565 expected_ntx_scripts,
566 account_code_upgrade,
567 };
568 request
569 .validate()
570 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
571
572 Ok(request)
573 }
574}
575
576pub(crate) fn collect_assets<'a>(
584 assets: impl Iterator<Item = &'a Asset>,
585) -> (BTreeMap<AccountId, u64>, Vec<Asset>) {
586 let mut fungible_balance_map = BTreeMap::new();
587 let mut non_fungible_set = Vec::new();
588
589 for asset in assets {
590 if let Some(fungible) = asset.as_fungible() {
591 let amount = fungible.amount().as_u64();
592 fungible_balance_map
593 .entry(fungible.faucet_id())
594 .and_modify(|balance| *balance += amount)
595 .or_insert(amount);
596 } else if asset.is_non_fungible() && !non_fungible_set.contains(asset) {
597 non_fungible_set.push(*asset);
598 }
599 }
600
601 (fungible_balance_map, non_fungible_set)
602}
603
604impl Default for TransactionRequestBuilder {
605 fn default() -> Self {
606 Self::new()
607 }
608}
609
610#[derive(Debug, Error)]
615pub enum TransactionRequestError {
616 #[error("failed to build the send-notes transaction script")]
617 SendNotesTransactionScriptError(#[from] SendNotesTransactionScriptError),
618 #[error("account error")]
619 AccountError(#[from] AccountError),
620 #[error("asset error")]
621 AssetError(#[from] AssetError),
622 #[error("duplicate input note: note {0} was added more than once to the transaction")]
623 DuplicateInputNote(NoteId),
624 #[error("transaction expiration delta must be greater than zero")]
625 ZeroExpirationDelta,
626 #[error(
627 "the account proof does not contain the required foreign account data; re-fetch the proof and retry"
628 )]
629 ForeignAccountDataMissing,
630 #[error(
631 "foreign account {0} has incompatible visibility; use `ForeignAccount::public()` for public accounts and `ForeignAccount::private()` for private accounts"
632 )]
633 InvalidForeignAccountId(AccountId),
634 #[error(
635 "inputs for foreign account {account_id} do not open against the account tree of the \
636 transaction's reference block {block_num}"
637 )]
638 ForeignAccountNotAtReferenceBlock {
639 account_id: AccountId,
640 block_num: BlockNumber,
641 },
642 #[error(
643 "note {0} cannot be used as an authenticated input: it does not have a valid inclusion proof"
644 )]
645 InputNoteNotAuthenticated(NoteId),
646 #[error("note with details commitment {} has already been consumed", .0.to_hex())]
647 InputNoteAlreadyConsumed(NoteDetailsCommitment),
648 #[error(
649 "note with details commitment {} is being consumed by pending transaction {transaction_id}",
650 note.to_hex()
651 )]
652 InputNoteBeingProcessed {
653 note: NoteDetailsCommitment,
654 transaction_id: TransactionId,
655 },
656 #[error(
657 "output note declares sender {actual} but the transaction is executed by account {expected}"
658 )]
659 OutputNoteSenderMismatch { expected: AccountId, actual: AccountId },
660 #[error(
661 "the request declares a fee conversion salt but the account's auth component {0} does not \
662 read the auth args as fee conversion info"
663 )]
664 FeeConversionInfoUnsupported(String),
665 #[error(
666 "account's `{0}` component reuses the fee conversion salt as a replay guard, so the \
667 caller must declare a fresh one with `TransactionRequestBuilder::fee_conversion_salt`"
668 )]
669 FeeConversionInfoRequired(String),
670 #[error("merkle proof error")]
671 MerkleError(#[from] MerkleError),
672 #[error("empty transaction: the request has no input notes and no account state changes")]
673 NoInputNotesNorAccountChange,
674 #[error("failed to create note")]
675 NoteCreationError(#[from] NoteError),
676 #[error("note failed validation")]
677 NoteValidationError(#[source] NoteError),
678 #[error("note execution failed")]
679 NoteExecutionError(#[source] NoteError),
680 #[error("failed to build note args")]
681 NoteArgError(#[source] NoteError),
682 #[error("pay-to-ID note must contain at least one asset to transfer")]
683 P2IDNoteWithoutAsset,
684 #[error("swap note assets must be non-zero: a zero {0} asset makes the exchange one-sided")]
685 SwapNoteWithZeroAsset(&'static str),
686 #[error(
687 "non-fungible asset issued by faucet {0} is not available in the account vault or incoming notes"
688 )]
689 MissingNonFungibleAsset(AccountId),
690 #[error("PSWAP note can only be cancelled by its creator: expected {expected}, got {actual}")]
691 PswapCancelCreatorMismatch { expected: AccountId, actual: AccountId },
692 #[error("error building script")]
693 CodeBuilderError(#[from] CodeBuilderError),
694 #[error("transaction script template error: {0}")]
695 ScriptTemplateError(String),
696 #[error("foreign procedure takes at most {max} input felts, got {actual}")]
697 ForeignProcedureInputsTooLong { max: usize, actual: usize },
698 #[error("storage slot {0} not found in account ID {1}")]
699 StorageSlotNotFound(u8, AccountId),
700 #[error("error while building the input notes")]
701 TransactionInputError(#[from] TransactionInputError),
702 #[error("account storage map error")]
703 StorageMapError(#[from] StorageMapError),
704}
705
706#[cfg(test)]
710mod tests {
711 use std::vec::Vec;
712
713 use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment};
714 use miden_protocol::account::{
715 AccountBuilder,
716 AccountComponent,
717 AccountId,
718 AccountType,
719 StorageMapKey,
720 StorageSlotName,
721 };
722 use miden_protocol::asset::FungibleAsset;
723 use miden_protocol::block::account_tree::AccountTree;
724 use miden_protocol::crypto::SequentialCommit;
725 use miden_protocol::note::{NoteTag, NoteType};
726 use miden_protocol::testing::account_id::{
727 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
728 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
729 ACCOUNT_ID_SENDER,
730 };
731 use miden_protocol::transaction::{AccountInputs, InputNote};
732 use miden_protocol::{EMPTY_WORD, Felt, Word};
733 use miden_standards::account::auth::{Approver, AuthSingleSig, MultisigAuthArgs};
734 use miden_standards::note::P2idNote;
735 use miden_standards::testing::account_component::MockAccountComponent;
736 use miden_tx::utils::serde::{Deserializable, Serializable};
737 use rand::SeedableRng;
738 use rand_chacha::ChaCha20Rng;
739
740 use super::{
741 BlockNumber,
742 ExpirationTransactionScript,
743 NonZeroU16,
744 TransactionRequest,
745 TransactionRequestBuilder,
746 TransactionScript,
747 };
748 use crate::rng::draw_word;
749 use crate::rpc::domain::account::AccountStorageRequirements;
750 use crate::transaction::ForeignAccount;
751
752 #[test]
753 fn transaction_request_serialization() {
754 assert_transaction_request_serialization_with(|| {
755 AuthSingleSig::new(Approver::new(
756 PublicKeyCommitment::from(EMPTY_WORD),
757 AuthScheme::Falcon512Poseidon2,
758 ))
759 .into()
760 });
761 }
762
763 #[test]
764 fn transaction_request_serialization_ecdsa() {
765 assert_transaction_request_serialization_with(|| {
766 AuthSingleSig::new(Approver::new(
767 PublicKeyCommitment::from(EMPTY_WORD),
768 AuthScheme::EcdsaK256Keccak,
769 ))
770 .into()
771 });
772 }
773
774 #[test]
775 fn auth_args_set_the_commitment_and_its_preimage() {
776 let auth_args = MultisigAuthArgs::new(BlockNumber::from(7u32), Word::from([1u32, 2, 3, 4]));
777
778 let request = TransactionRequestBuilder::new()
779 .fee_conversion_salt(Word::from([5u32, 6, 7, 8]))
780 .auth_args(&auth_args)
781 .build()
782 .unwrap();
783
784 let auth_arg = auth_args.to_commitment();
785 assert_eq!(request.auth_arg(), &Some(auth_arg));
786 assert_eq!(request.fee_conversion_salt(), None);
787 assert_eq!(request.advice_map().get(&auth_arg).unwrap().as_ref(), auth_args.to_elements());
788 }
789
790 #[test]
791 fn expiration_delta_without_script_template_builds_expiration_script() {
792 let account = AccountBuilder::new(Default::default())
793 .with_component(MockAccountComponent::with_empty_slots())
794 .with_component(AuthSingleSig::new(Approver::new(
795 PublicKeyCommitment::from(EMPTY_WORD),
796 AuthScheme::Falcon512Poseidon2,
797 )))
798 .account_type(AccountType::Private)
799 .build_existing()
800 .unwrap();
801 let code_interface = account.code_interface();
802
803 let delta = NonZeroU16::new(9).unwrap();
804 let tx_request =
805 TransactionRequestBuilder::new().expiration_delta(delta.get()).build().unwrap();
806
807 let (script, script_args) =
808 tx_request.build_transaction_script(&code_interface).unwrap().unwrap();
809 let expected = ExpirationTransactionScript::new(delta);
810 assert_eq!(script.root(), TransactionScript::from(expected).root());
811 assert_eq!(script_args, Some(expected.tx_script_args()));
812
813 let tx_request = TransactionRequestBuilder::new().build().unwrap();
815 assert!(tx_request.build_transaction_script(&code_interface).unwrap().is_none());
816 }
817
818 #[test]
819 fn deserialization_rejects_duplicate_input_notes() {
820 let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
821 let target_id =
822 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
823 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
824 let mut rng = ChaCha20Rng::seed_from_u64(0);
825 let note = P2idNote::builder()
826 .sender(sender_id)
827 .target(target_id)
828 .assets(vec![FungibleAsset::new(faucet_id, 100).unwrap()])
829 .note_type(NoteType::Private)
830 .serial_number(draw_word(&mut rng))
831 .build()
832 .unwrap();
833
834 let mut tx_request = TransactionRequestBuilder::new()
836 .input_notes(vec![(note.into(), None)])
837 .build()
838 .unwrap();
839 let note_id = tx_request.input_note_ids().next().unwrap();
840 tx_request.input_notes.push(tx_request.input_notes[0].clone());
841 tx_request.input_notes_args.push((note_id, None));
842
843 assert!(TransactionRequest::read_from_bytes(&tx_request.to_bytes()).is_err());
844 }
845
846 #[allow(deprecated)]
847 fn assert_transaction_request_serialization_with<F>(auth_component: F)
848 where
849 F: FnOnce() -> AccountComponent,
850 {
851 let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
852 let target_id =
853 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
854 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
855 let mut rng = ChaCha20Rng::seed_from_u64(0);
856
857 let mut notes = vec![];
858 for i in 0..7 {
859 let note = P2idNote::builder()
860 .sender(sender_id)
861 .target(target_id)
862 .assets(vec![FungibleAsset::new(faucet_id, 100 + i).unwrap()])
863 .note_type(NoteType::Private)
864 .serial_number(draw_word(&mut rng))
865 .build()
866 .expect("note creation failed");
867 notes.push(note.into());
868 }
869
870 let mut advice_vec: Vec<(Word, Vec<Felt>)> = vec![];
871 for i in 0u32..10 {
872 advice_vec.push((draw_word(&mut rng), vec![Felt::from(i)]));
873 }
874
875 let account = AccountBuilder::new(Default::default())
876 .with_component(MockAccountComponent::with_empty_slots())
877 .with_component(auth_component())
878 .account_type(AccountType::Private)
879 .build_existing()
880 .unwrap();
881
882 let tx_request = TransactionRequestBuilder::new()
884 .block_numbers([
885 BlockNumber::from(1u32),
886 BlockNumber::from(3u32),
887 BlockNumber::from(1u32),
888 ])
889 .input_notes(vec![(notes.pop().unwrap(), None)])
890 .explicit_input_notes(vec![(
891 InputNote::unauthenticated(notes.pop().unwrap()),
892 Some(draw_word(&mut rng)),
893 )])
894 .expected_output_recipients(vec![notes.pop().unwrap().recipient().clone()])
895 .expected_future_notes(vec![(
896 notes.pop().unwrap().into(),
897 NoteTag::with_account_target(sender_id),
898 )])
899 .extend_advice_map(advice_vec)
900 .foreign_accounts([
901 ForeignAccount::public(
902 target_id,
903 AccountStorageRequirements::new([(
904 StorageSlotName::new("demo::storage_slot").unwrap(),
905 &[StorageMapKey::new(Word::default())],
906 )]),
907 )
908 .unwrap(),
909 ForeignAccount::private(&account).unwrap(),
910 ])
911 .own_output_notes(vec![notes.pop().unwrap(), notes.pop().unwrap()])
912 .script_arg(draw_word(&mut rng))
913 .auth_arg(draw_word(&mut rng))
914 .expected_ntx_scripts(vec![notes.first().unwrap().recipient().script().clone()])
915 .build()
916 .unwrap();
917
918 let mut buffer = Vec::new();
919 tx_request.write_into(&mut buffer);
920
921 let deserialized_tx_request = TransactionRequest::read_from_bytes(&buffer).unwrap();
922 assert_eq!(tx_request, deserialized_tx_request);
923
924 let tree = AccountTree::with_entries([(account.id(), account.to_commitment())]).unwrap();
925 let inputs = AccountInputs::new((&account).into(), tree.open(account.id()));
926 let mut request = tx_request;
927 request.foreign_accounts.insert(inputs.id(), ForeignAccount::Prefetched(inputs));
928 let decoded = TransactionRequest::read_from_bytes(&request.to_bytes()).unwrap();
929 assert_eq!(request, decoded);
930 }
931}