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, AccountId};
10use miden_protocol::asset::Asset;
11use miden_protocol::crypto::merkle::MerkleError;
12use miden_protocol::crypto::merkle::store::MerkleStore;
13use miden_protocol::errors::{
14 AccountError,
15 AssetError,
16 AssetVaultError,
17 NoteError,
18 StorageMapError,
19 TransactionInputError,
20};
21use miden_protocol::note::{
22 Note,
23 NoteDetails,
24 NoteDetailsCommitment,
25 NoteId,
26 NoteRecipient,
27 NoteScript,
28 NoteTag,
29 PartialNote,
30};
31use miden_protocol::transaction::{
32 InputNote,
33 InputNotes,
34 TransactionArgs,
35 TransactionId,
36 TransactionScript,
37};
38use miden_protocol::vm::AdviceMap;
39use miden_protocol::{MastForestScriptError, Word};
40use miden_standards::account::auth::{FeeConversionInfo, commit_fee_conversion_info};
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;
57pub use builder::{
58 PaymentNoteDescription,
59 PswapTransactionData,
60 SwapTransactionData,
61 TransactionRequestBuilder,
62};
63
64mod foreign;
65pub(crate) use foreign::account_proof_into_inputs;
66pub use foreign::{ForeignAccount, build_fpi_script};
67
68use crate::store::InputNoteRecord;
69
70pub type NoteArgs = Word;
74
75#[derive(Clone, Debug, PartialEq, Eq)]
80pub enum TransactionScriptTemplate {
81 CustomScript(TransactionScript),
83 SendNotes(Vec<PartialNote>),
89}
90
91#[derive(Clone, Debug, PartialEq, Eq)]
97pub struct TransactionRequest {
98 input_notes: Vec<Note>,
103 input_notes_args: Vec<(NoteId, Option<NoteArgs>)>,
106 pub(super) explicit_input_notes: BTreeMap<NoteId, InputNote>,
108 script_template: Option<TransactionScriptTemplate>,
110 expected_output_recipients: BTreeMap<Word, NoteRecipient>,
112 expected_future_notes: BTreeMap<NoteDetailsCommitment, (NoteDetails, NoteTag)>,
117 advice_map: AdviceMap,
119 merkle_store: MerkleStore,
121 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
125 expiration_delta: Option<u16>,
128 ignore_invalid_input_notes: bool,
131 script_arg: Option<Word>,
134 auth_arg: Option<Word>,
137 fee_conversion_salt: Option<Word>,
141 expected_ntx_scripts: Vec<NoteScript>,
145}
146
147impl TransactionRequest {
148 pub fn input_notes(&self) -> &[Note] {
153 &self.input_notes
154 }
155
156 pub fn input_note_ids(&self) -> impl Iterator<Item = NoteId> {
158 self.input_notes.iter().map(Note::id)
159 }
160
161 pub fn incoming_assets(&self) -> (BTreeMap<AccountId, u64>, Vec<Asset>) {
163 collect_assets(self.input_notes.iter().flat_map(|note| note.assets().iter()))
164 }
165
166 pub fn get_note_args(&self) -> BTreeMap<NoteId, NoteArgs> {
169 self.input_notes_args
170 .iter()
171 .filter_map(|(note, args)| args.map(|a| (*note, a)))
172 .collect()
173 }
174
175 pub fn expected_output_own_notes(&self) -> Vec<Note> {
180 match &self.script_template {
181 Some(TransactionScriptTemplate::SendNotes(notes)) => notes
182 .iter()
183 .map(|partial| {
184 Note::with_attachments(
185 partial.assets().clone(),
186 *partial.partial_metadata(),
187 self.expected_output_recipients
188 .get(&partial.recipient_digest())
189 .expect("Recipient should be included if it's an own note")
190 .clone(),
191 partial.attachments().clone(),
192 )
193 })
194 .collect(),
195 _ => vec![],
196 }
197 }
198
199 pub fn expected_output_recipients(&self) -> impl Iterator<Item = &NoteRecipient> {
201 self.expected_output_recipients.values()
202 }
203
204 pub fn expected_future_notes(&self) -> impl Iterator<Item = &(NoteDetails, NoteTag)> {
206 self.expected_future_notes.values()
207 }
208
209 pub fn script_template(&self) -> &Option<TransactionScriptTemplate> {
211 &self.script_template
212 }
213
214 pub fn advice_map(&self) -> &AdviceMap {
216 &self.advice_map
217 }
218
219 pub fn advice_map_mut(&mut self) -> &mut AdviceMap {
221 &mut self.advice_map
222 }
223
224 pub fn merkle_store(&self) -> &MerkleStore {
226 &self.merkle_store
227 }
228
229 pub fn foreign_accounts(&self) -> &BTreeMap<AccountId, ForeignAccount> {
231 &self.foreign_accounts
232 }
233
234 pub fn ignore_invalid_input_notes(&self) -> bool {
236 self.ignore_invalid_input_notes
237 }
238
239 pub fn script_arg(&self) -> &Option<Word> {
241 &self.script_arg
242 }
243
244 pub fn auth_arg(&self) -> &Option<Word> {
246 &self.auth_arg
247 }
248
249 pub fn fee_conversion_salt(&self) -> Option<Word> {
252 self.fee_conversion_salt
253 }
254
255 pub fn has_auth_arg(&self) -> bool {
260 self.auth_arg.is_some_and(|auth_arg| auth_arg != Word::empty())
261 }
262
263 pub fn expected_ntx_scripts(&self) -> &[NoteScript] {
265 &self.expected_ntx_scripts
266 }
267
268 #[cfg(feature = "testing")]
276 pub(crate) fn add_unauthenticated_input_note(&mut self, note: Note) {
277 self.input_notes_args.push((note.id(), None));
278 self.input_notes.push(note);
279 }
280
281 pub(crate) fn commit_native_fee_conversion_info(
284 &mut self,
285 fee_faucet_id: AccountId,
286 salt: Word,
287 ) {
288 let (auth_arg, preimage) =
289 commit_fee_conversion_info(FeeConversionInfo::one_to_one(fee_faucet_id), salt);
290 self.advice_map.insert(auth_arg, preimage);
291 self.auth_arg = Some(auth_arg);
292 }
293
294 fn validate(&self) -> Result<(), TransactionRequestError> {
299 let mut seen_input_notes = BTreeSet::new();
300 for (note_id, _) in &self.input_notes_args {
301 if !seen_input_notes.insert(note_id) {
302 return Err(TransactionRequestError::DuplicateInputNote(*note_id));
303 }
304 }
305
306 Ok(())
307 }
308
309 pub(crate) fn build_input_notes(
315 &self,
316 authenticated_note_records: Vec<InputNoteRecord>,
317 ) -> Result<InputNotes<InputNote>, TransactionRequestError> {
318 let mut authenticated_notes: BTreeMap<NoteId, InputNoteRecord> = BTreeMap::new();
319 for record in authenticated_note_records {
320 let note_id =
323 record.id().expect("authenticated note record carries metadata so id() is Some");
324
325 if !record.is_authenticated() {
326 return Err(TransactionRequestError::InputNoteNotAuthenticated(note_id));
327 }
328 if record.is_consumed() {
329 return Err(TransactionRequestError::InputNoteAlreadyConsumed(
330 record.details_commitment(),
331 ));
332 }
333
334 authenticated_notes.insert(note_id, record);
335 }
336
337 let input_notes = self
338 .input_notes()
339 .iter()
340 .map(|note| match self.explicit_input_notes.get(¬e.id()) {
341 Some(input_note) => input_note.clone(),
342 None => match authenticated_notes.remove(¬e.id()) {
343 Some(record) => record
344 .try_into()
345 .expect("Authenticated note record should be convertible to InputNote"),
346 None => InputNote::unauthenticated(note.clone()),
347 },
348 })
349 .collect();
350
351 Ok(InputNotes::new(input_notes)?)
352 }
353
354 pub(crate) fn into_transaction_args(
357 self,
358 tx_script: Option<(TransactionScript, Option<Word>)>,
359 ) -> TransactionArgs {
360 let note_args = self.get_note_args();
361 let TransactionRequest {
362 expected_output_recipients,
363 advice_map,
364 merkle_store,
365 ..
366 } = self;
367
368 let mut tx_args = TransactionArgs::new(advice_map).with_note_args(note_args);
369
370 if let Some((tx_script, script_args)) = tx_script {
374 let script_args = script_args.or(self.script_arg).unwrap_or_default();
375 tx_args = tx_args.with_tx_script_and_args(tx_script, script_args);
376 }
377
378 if let Some(auth_argument) = self.auth_arg {
379 tx_args = tx_args.with_auth_args(auth_argument);
380 }
381
382 tx_args
383 .extend_output_note_recipients(expected_output_recipients.into_values().map(Box::new));
384 tx_args.extend_merkle_store(merkle_store.inner_nodes());
385
386 tx_args
387 }
388
389 pub(crate) fn build_transaction_script(
407 &self,
408 code_interface: &AccountCodeInterface,
409 ) -> Result<Option<(TransactionScript, Option<Word>)>, TransactionRequestError> {
410 match &self.script_template {
411 Some(TransactionScriptTemplate::CustomScript(script)) => {
412 Ok(Some((script.clone(), None)))
413 },
414 Some(TransactionScriptTemplate::SendNotes(notes)) => {
415 let script = match self.expiration_delta.and_then(NonZeroU16::new) {
416 Some(delta) => SendNotesTransactionScript::with_expiration_delta(
417 code_interface,
418 notes,
419 delta,
420 )?,
421 None => SendNotesTransactionScript::new(code_interface, notes)?,
422 };
423 Ok(Some((script.tx_script().clone(), Some(script.tx_script_args()))))
424 },
425 None => match self.expiration_delta.and_then(NonZeroU16::new) {
426 Some(delta) => {
427 let script = ExpirationTransactionScript::new(delta);
428 Ok(Some((script.into(), Some(script.tx_script_args()))))
429 },
430 None => Ok(None),
431 },
432 }
433 }
434}
435
436impl Serializable for TransactionRequest {
440 fn write_into<W: ByteWriter>(&self, target: &mut W) {
441 self.input_notes.write_into(target);
442 self.input_notes_args.write_into(target);
443 self.explicit_input_notes.write_into(target);
444 match &self.script_template {
445 None => target.write_u8(0),
446 Some(TransactionScriptTemplate::CustomScript(script)) => {
447 target.write_u8(1);
448 script.write_into(target);
449 },
450 Some(TransactionScriptTemplate::SendNotes(notes)) => {
451 target.write_u8(2);
452 notes.write_into(target);
453 },
454 }
455 self.expected_output_recipients.write_into(target);
456 self.expected_future_notes.write_into(target);
457 self.advice_map.write_into(target);
458 self.merkle_store.write_into(target);
459 let foreign_accounts: Vec<_> = self.foreign_accounts.values().cloned().collect();
460 foreign_accounts.write_into(target);
461 self.expiration_delta.write_into(target);
462 target.write_u8(u8::from(self.ignore_invalid_input_notes));
463 self.script_arg.write_into(target);
464 self.auth_arg.write_into(target);
465 self.fee_conversion_salt.write_into(target);
466 self.expected_ntx_scripts.write_into(target);
467 }
468}
469
470impl Deserializable for TransactionRequest {
471 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
472 let input_notes = Vec::<Note>::read_from(source)?;
473 let input_notes_args = Vec::<(NoteId, Option<NoteArgs>)>::read_from(source)?;
474 let explicit_input_notes = BTreeMap::<NoteId, InputNote>::read_from(source)?;
475 for (note_id, input_note) in &explicit_input_notes {
476 if *note_id != input_note.id() || !input_notes.contains(input_note.note()) {
477 return Err(DeserializationError::InvalidValue(format!(
478 "explicit input note {note_id} does not match a request input note"
479 )));
480 }
481 }
482
483 let script_template = match source.read_u8()? {
484 0 => None,
485 1 => {
486 let transaction_script = TransactionScript::read_from(source)?;
487 Some(TransactionScriptTemplate::CustomScript(transaction_script))
488 },
489 2 => {
490 let notes = Vec::<PartialNote>::read_from(source)?;
491 Some(TransactionScriptTemplate::SendNotes(notes))
492 },
493 _ => {
494 return Err(DeserializationError::InvalidValue(
495 "Invalid script template type".to_string(),
496 ));
497 },
498 };
499
500 let expected_output_recipients = BTreeMap::<Word, NoteRecipient>::read_from(source)?;
501 let expected_future_notes =
502 BTreeMap::<NoteDetailsCommitment, (NoteDetails, NoteTag)>::read_from(source)?;
503
504 let advice_map = AdviceMap::read_from(source)?;
505 let merkle_store = MerkleStore::read_from(source)?;
506 let mut foreign_accounts = BTreeMap::new();
507 for foreign_account in Vec::<ForeignAccount>::read_from(source)? {
508 foreign_accounts.entry(foreign_account.account_id()).or_insert(foreign_account);
509 }
510 let expiration_delta = Option::<u16>::read_from(source)?;
511 let ignore_invalid_input_notes = source.read_u8()? == 1;
512 let script_arg = Option::<Word>::read_from(source)?;
513 let auth_arg = Option::<Word>::read_from(source)?;
514 let fee_conversion_salt = Option::<Word>::read_from(source)?;
515 let expected_ntx_scripts = Vec::<NoteScript>::read_from(source)?;
516
517 let request = TransactionRequest {
518 input_notes,
519 input_notes_args,
520 explicit_input_notes,
521 script_template,
522 expected_output_recipients,
523 expected_future_notes,
524 advice_map,
525 merkle_store,
526 foreign_accounts,
527 expiration_delta,
528 ignore_invalid_input_notes,
529 script_arg,
530 auth_arg,
531 fee_conversion_salt,
532 expected_ntx_scripts,
533 };
534 request
535 .validate()
536 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
537
538 Ok(request)
539 }
540}
541
542pub(crate) fn collect_assets<'a>(
550 assets: impl Iterator<Item = &'a Asset>,
551) -> (BTreeMap<AccountId, u64>, Vec<Asset>) {
552 let mut fungible_balance_map = BTreeMap::new();
553 let mut non_fungible_set = Vec::new();
554
555 for asset in assets {
556 if let Some(fungible) = asset.as_fungible() {
557 let amount = fungible.amount().as_u64();
558 fungible_balance_map
559 .entry(fungible.faucet_id())
560 .and_modify(|balance| *balance += amount)
561 .or_insert(amount);
562 } else if asset.is_non_fungible() && !non_fungible_set.contains(asset) {
563 non_fungible_set.push(*asset);
564 }
565 }
566
567 (fungible_balance_map, non_fungible_set)
568}
569
570impl Default for TransactionRequestBuilder {
571 fn default() -> Self {
572 Self::new()
573 }
574}
575
576#[derive(Debug, Error)]
581pub enum TransactionRequestError {
582 #[error("failed to build the send-notes transaction script")]
583 SendNotesTransactionScriptError(#[from] SendNotesTransactionScriptError),
584 #[error("account error")]
585 AccountError(#[from] AccountError),
586 #[error("asset error")]
587 AssetError(#[from] AssetError),
588 #[error("duplicate input note: note {0} was added more than once to the transaction")]
589 DuplicateInputNote(NoteId),
590 #[error("transaction expiration delta must be greater than zero")]
591 ZeroExpirationDelta,
592 #[error(
593 "the account proof does not contain the required foreign account data; re-fetch the proof and retry"
594 )]
595 ForeignAccountDataMissing,
596 #[error(
597 "foreign account {0} has incompatible visibility; use `ForeignAccount::public()` for public accounts and `ForeignAccount::private()` for private accounts"
598 )]
599 InvalidForeignAccountId(AccountId),
600 #[error(
601 "note {0} cannot be used as an authenticated input: it does not have a valid inclusion proof"
602 )]
603 InputNoteNotAuthenticated(NoteId),
604 #[error("note with details commitment {} has already been consumed", .0.to_hex())]
605 InputNoteAlreadyConsumed(NoteDetailsCommitment),
606 #[error(
607 "note with details commitment {} is being consumed by pending transaction {transaction_id}",
608 note.to_hex()
609 )]
610 InputNoteBeingProcessed {
611 note: NoteDetailsCommitment,
612 transaction_id: TransactionId,
613 },
614 #[error(
615 "output note declares sender {actual} but the transaction is executed by account {expected}"
616 )]
617 OutputNoteSenderMismatch { expected: AccountId, actual: AccountId },
618 #[error(
619 "the request declares a fee conversion salt but the account's auth component {0} does not \
620 read the auth args as fee conversion info"
621 )]
622 FeeConversionInfoUnsupported(String),
623 #[error(
624 "account's `{0}` component reuses the fee conversion salt as a replay guard, so the \
625 caller must declare a fresh one with `TransactionRequestBuilder::fee_conversion_salt`"
626 )]
627 FeeConversionInfoRequired(String),
628 #[error("invalid transaction script")]
629 InvalidTransactionScript(#[from] MastForestScriptError),
630 #[error("merkle proof error")]
631 MerkleError(#[from] MerkleError),
632 #[error("empty transaction: the request has no input notes and no account state changes")]
633 NoInputNotesNorAccountChange,
634 #[error("note not found: {0}")]
635 NoteNotFound(String),
636 #[error("failed to create note")]
637 NoteCreationError(#[from] NoteError),
638 #[error("note failed validation")]
639 NoteValidationError(#[source] NoteError),
640 #[error("note execution failed")]
641 NoteExecutionError(#[source] NoteError),
642 #[error("failed to build note args")]
643 NoteArgError(#[source] NoteError),
644 #[error("pay-to-ID note must contain at least one asset to transfer")]
645 P2IDNoteWithoutAsset,
646 #[error("swap note assets must be non-zero: a zero {0} asset makes the exchange one-sided")]
647 SwapNoteWithZeroAsset(&'static str),
648 #[error(
649 "non-fungible asset issued by faucet {0} is not available in the account vault or incoming notes"
650 )]
651 MissingNonFungibleAsset(AccountId),
652 #[error("PSWAP note can only be cancelled by its creator: expected {expected}, got {actual}")]
653 PswapCancelCreatorMismatch { expected: AccountId, actual: AccountId },
654 #[error("error building script")]
655 CodeBuilderError(#[from] CodeBuilderError),
656 #[error("transaction script template error: {0}")]
657 ScriptTemplateError(String),
658 #[error("foreign procedure takes at most {max} input felts, got {actual}")]
659 ForeignProcedureInputsTooLong { max: usize, actual: usize },
660 #[error("storage slot {0} not found in account ID {1}")]
661 StorageSlotNotFound(u8, AccountId),
662 #[error("error while building the input notes")]
663 TransactionInputError(#[from] TransactionInputError),
664 #[error("account storage map error")]
665 StorageMapError(#[from] StorageMapError),
666 #[error("asset vault error")]
667 AssetVaultError(#[from] AssetVaultError),
668 #[error(
669 "unsupported authentication scheme ID {0}; supported schemes are: RpoFalcon512 (0) and EcdsaK256Keccak (1)"
670 )]
671 UnsupportedAuthSchemeId(u8),
672}
673
674#[cfg(test)]
678mod tests {
679 use std::vec::Vec;
680
681 use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment};
682 use miden_protocol::account::{
683 AccountBuilder,
684 AccountComponent,
685 AccountId,
686 AccountType,
687 StorageMapKey,
688 StorageSlotName,
689 };
690 use miden_protocol::asset::FungibleAsset;
691 use miden_protocol::crypto::rand::{FeltRng, RandomCoin};
692 use miden_protocol::note::{NoteTag, NoteType};
693 use miden_protocol::testing::account_id::{
694 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
695 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
696 ACCOUNT_ID_SENDER,
697 };
698 use miden_protocol::transaction::InputNote;
699 use miden_protocol::{EMPTY_WORD, Felt, Word};
700 use miden_standards::account::auth::{Approver, AuthSingleSig};
701 use miden_standards::note::P2idNote;
702 use miden_standards::testing::account_component::MockAccountComponent;
703 use miden_tx::utils::serde::{Deserializable, Serializable};
704
705 use super::{
706 ExpirationTransactionScript,
707 NonZeroU16,
708 TransactionRequest,
709 TransactionRequestBuilder,
710 TransactionScript,
711 };
712 use crate::rpc::domain::account::AccountStorageRequirements;
713 use crate::transaction::ForeignAccount;
714
715 #[test]
716 fn transaction_request_serialization() {
717 assert_transaction_request_serialization_with(|| {
718 AuthSingleSig::new(Approver::new(
719 PublicKeyCommitment::from(EMPTY_WORD),
720 AuthScheme::Falcon512Poseidon2,
721 ))
722 .into()
723 });
724 }
725
726 #[test]
727 fn transaction_request_serialization_ecdsa() {
728 assert_transaction_request_serialization_with(|| {
729 AuthSingleSig::new(Approver::new(
730 PublicKeyCommitment::from(EMPTY_WORD),
731 AuthScheme::EcdsaK256Keccak,
732 ))
733 .into()
734 });
735 }
736
737 #[test]
738 fn expiration_delta_without_script_template_builds_expiration_script() {
739 let account = AccountBuilder::new(Default::default())
740 .with_component(MockAccountComponent::with_empty_slots())
741 .with_component(AuthSingleSig::new(Approver::new(
742 PublicKeyCommitment::from(EMPTY_WORD),
743 AuthScheme::Falcon512Poseidon2,
744 )))
745 .account_type(AccountType::Private)
746 .build_existing()
747 .unwrap();
748 let code_interface = account.code_interface();
749
750 let delta = NonZeroU16::new(9).unwrap();
751 let tx_request =
752 TransactionRequestBuilder::new().expiration_delta(delta.get()).build().unwrap();
753
754 let (script, script_args) =
755 tx_request.build_transaction_script(&code_interface).unwrap().unwrap();
756 let expected = ExpirationTransactionScript::new(delta);
757 assert_eq!(script.root(), TransactionScript::from(expected).root());
758 assert_eq!(script_args, Some(expected.tx_script_args()));
759
760 let tx_request = TransactionRequestBuilder::new().build().unwrap();
762 assert!(tx_request.build_transaction_script(&code_interface).unwrap().is_none());
763 }
764
765 #[test]
766 fn deserialization_rejects_duplicate_input_notes() {
767 let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
768 let target_id =
769 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
770 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
771 let note = P2idNote::builder()
772 .sender(sender_id)
773 .target(target_id)
774 .assets(vec![FungibleAsset::new(faucet_id, 100).unwrap()])
775 .note_type(NoteType::Private)
776 .generate_serial_number(&mut RandomCoin::new(Word::default()))
777 .build()
778 .unwrap();
779
780 let mut tx_request = TransactionRequestBuilder::new()
782 .input_notes(vec![(note.into(), None)])
783 .build()
784 .unwrap();
785 let note_id = tx_request.input_note_ids().next().unwrap();
786 tx_request.input_notes.push(tx_request.input_notes[0].clone());
787 tx_request.input_notes_args.push((note_id, None));
788
789 assert!(TransactionRequest::read_from_bytes(&tx_request.to_bytes()).is_err());
790 }
791
792 fn assert_transaction_request_serialization_with<F>(auth_component: F)
793 where
794 F: FnOnce() -> AccountComponent,
795 {
796 let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
797 let target_id =
798 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
799 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
800 let mut rng = RandomCoin::new(Word::default());
801
802 let mut notes = vec![];
803 for i in 0..7 {
804 let note = P2idNote::builder()
805 .sender(sender_id)
806 .target(target_id)
807 .assets(vec![FungibleAsset::new(faucet_id, 100 + i).unwrap()])
808 .note_type(NoteType::Private)
809 .generate_serial_number(&mut rng)
810 .build()
811 .expect("note creation failed");
812 notes.push(note.into());
813 }
814
815 let mut advice_vec: Vec<(Word, Vec<Felt>)> = vec![];
816 for i in 0u32..10 {
817 advice_vec.push((rng.draw_word(), vec![Felt::from(i)]));
818 }
819
820 let account = AccountBuilder::new(Default::default())
821 .with_component(MockAccountComponent::with_empty_slots())
822 .with_component(auth_component())
823 .account_type(AccountType::Private)
824 .build_existing()
825 .unwrap();
826
827 let tx_request = TransactionRequestBuilder::new()
829 .input_notes(vec![(notes.pop().unwrap(), None)])
830 .explicit_input_notes(vec![(
831 InputNote::unauthenticated(notes.pop().unwrap()),
832 Some(rng.draw_word()),
833 )])
834 .expected_output_recipients(vec![notes.pop().unwrap().recipient().clone()])
835 .expected_future_notes(vec![(
836 notes.pop().unwrap().into(),
837 NoteTag::with_account_target(sender_id),
838 )])
839 .extend_advice_map(advice_vec)
840 .foreign_accounts([
841 ForeignAccount::public(
842 target_id,
843 AccountStorageRequirements::new([(
844 StorageSlotName::new("demo::storage_slot").unwrap(),
845 &[StorageMapKey::new(Word::default())],
846 )]),
847 )
848 .unwrap(),
849 ForeignAccount::private(&account).unwrap(),
850 ])
851 .own_output_notes(vec![notes.pop().unwrap(), notes.pop().unwrap()])
852 .script_arg(rng.draw_word())
853 .auth_arg(rng.draw_word())
854 .expected_ntx_scripts(vec![notes.first().unwrap().recipient().script().clone()])
855 .build()
856 .unwrap();
857
858 let mut buffer = Vec::new();
859 tx_request.write_into(&mut buffer);
860
861 let deserialized_tx_request = TransactionRequest::read_from_bytes(&buffer).unwrap();
862 assert_eq!(tx_request, deserialized_tx_request);
863 }
864}