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