1use alloc::boxed::Box;
4use alloc::collections::{BTreeMap, BTreeSet};
5use alloc::string::{String, ToString};
6use alloc::vec::Vec;
7
8use miden_protocol::Word;
9use miden_protocol::account::AccountId;
10use miden_protocol::asset::{Asset, NonFungibleAsset};
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 TransactionScriptError,
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::{InputNote, InputNotes, TransactionArgs, TransactionScript};
33use miden_protocol::vm::AdviceMap;
34use miden_standards::account::interface::{AccountInterface, AccountInterfaceError};
35use miden_standards::errors::CodeBuilderError;
36use miden_tx::utils::serde::{
37 ByteReader,
38 ByteWriter,
39 Deserializable,
40 DeserializationError,
41 Serializable,
42};
43use thiserror::Error;
44
45mod builder;
46pub use builder::{
47 PaymentNoteDescription,
48 PswapTransactionData,
49 SwapTransactionData,
50 TransactionRequestBuilder,
51};
52
53mod foreign;
54pub use foreign::ForeignAccount;
55pub(crate) use foreign::account_proof_into_inputs;
56
57use crate::store::InputNoteRecord;
58
59pub type NoteArgs = Word;
63
64#[derive(Clone, Debug, PartialEq, Eq)]
69pub enum TransactionScriptTemplate {
70 CustomScript(TransactionScript),
72 SendNotes(Vec<PartialNote>),
78}
79
80#[derive(Clone, Debug, PartialEq, Eq)]
86pub struct TransactionRequest {
87 input_notes: Vec<Note>,
92 input_notes_args: Vec<(NoteId, Option<NoteArgs>)>,
95 script_template: Option<TransactionScriptTemplate>,
97 expected_output_recipients: BTreeMap<Word, NoteRecipient>,
99 expected_future_notes: BTreeMap<NoteDetailsCommitment, (NoteDetails, NoteTag)>,
104 advice_map: AdviceMap,
106 merkle_store: MerkleStore,
108 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
112 expiration_delta: Option<u16>,
115 ignore_invalid_input_notes: bool,
119 script_arg: Option<Word>,
122 auth_arg: Option<Word>,
125 expected_ntx_scripts: Vec<NoteScript>,
129}
130
131impl TransactionRequest {
132 pub fn input_notes(&self) -> &[Note] {
137 &self.input_notes
138 }
139
140 pub fn input_note_ids(&self) -> impl Iterator<Item = NoteId> {
142 self.input_notes.iter().map(Note::id)
143 }
144
145 pub fn incoming_assets(&self) -> (BTreeMap<AccountId, u64>, Vec<NonFungibleAsset>) {
147 collect_assets(self.input_notes.iter().flat_map(|note| note.assets().iter()))
148 }
149
150 pub fn get_note_args(&self) -> BTreeMap<NoteId, NoteArgs> {
153 self.input_notes_args
154 .iter()
155 .filter_map(|(note, args)| args.map(|a| (*note, a)))
156 .collect()
157 }
158
159 pub fn expected_output_own_notes(&self) -> Vec<Note> {
165 match &self.script_template {
166 Some(TransactionScriptTemplate::SendNotes(notes)) => notes
167 .iter()
168 .map(|partial| {
169 Note::with_attachments(
170 partial.assets().clone(),
171 *partial.partial_metadata(),
172 self.expected_output_recipients
173 .get(&partial.recipient_digest())
174 .expect("Recipient should be included if it's an own note")
175 .clone(),
176 partial.attachments().clone(),
177 )
178 })
179 .collect(),
180 _ => vec![],
181 }
182 }
183
184 pub fn expected_output_recipients(&self) -> impl Iterator<Item = &NoteRecipient> {
186 self.expected_output_recipients.values()
187 }
188
189 pub fn expected_future_notes(&self) -> impl Iterator<Item = &(NoteDetails, NoteTag)> {
191 self.expected_future_notes.values()
192 }
193
194 pub fn script_template(&self) -> &Option<TransactionScriptTemplate> {
196 &self.script_template
197 }
198
199 pub fn advice_map(&self) -> &AdviceMap {
201 &self.advice_map
202 }
203
204 pub fn advice_map_mut(&mut self) -> &mut AdviceMap {
206 &mut self.advice_map
207 }
208
209 pub fn merkle_store(&self) -> &MerkleStore {
211 &self.merkle_store
212 }
213
214 pub fn foreign_accounts(&self) -> &BTreeMap<AccountId, ForeignAccount> {
216 &self.foreign_accounts
217 }
218
219 pub fn ignore_invalid_input_notes(&self) -> bool {
221 self.ignore_invalid_input_notes
222 }
223
224 pub fn script_arg(&self) -> &Option<Word> {
226 &self.script_arg
227 }
228
229 pub fn auth_arg(&self) -> &Option<Word> {
231 &self.auth_arg
232 }
233
234 pub fn expected_ntx_scripts(&self) -> &[NoteScript] {
236 &self.expected_ntx_scripts
237 }
238
239 pub(crate) fn build_input_notes(
246 &self,
247 authenticated_note_records: Vec<InputNoteRecord>,
248 ) -> Result<InputNotes<InputNote>, TransactionRequestError> {
249 let mut input_notes: BTreeMap<NoteId, InputNote> = BTreeMap::new();
250
251 for authenticated_note_record in authenticated_note_records {
253 let authenticated_note_id = authenticated_note_record
256 .id()
257 .expect("authenticated note record carries metadata so id() is Some");
258
259 if !authenticated_note_record.is_authenticated() {
260 return Err(TransactionRequestError::InputNoteNotAuthenticated(
261 authenticated_note_id,
262 ));
263 }
264
265 if authenticated_note_record.is_consumed() {
266 return Err(TransactionRequestError::InputNoteAlreadyConsumed(
267 authenticated_note_id,
268 ));
269 }
270
271 input_notes.insert(
272 authenticated_note_id,
273 authenticated_note_record
274 .try_into()
275 .expect("Authenticated note record should be convertible to InputNote"),
276 );
277 }
278
279 let authenticated_note_ids: BTreeSet<NoteId> = input_notes.keys().copied().collect();
281 for note in self.input_notes().iter().filter(|n| !authenticated_note_ids.contains(&n.id()))
282 {
283 input_notes.insert(note.id(), InputNote::Unauthenticated { note: note.clone() });
284 }
285
286 Ok(InputNotes::new(
287 self.input_note_ids()
288 .map(|note_id| {
289 input_notes
290 .remove(¬e_id)
291 .expect("The input note map was checked to contain all input notes")
292 })
293 .collect(),
294 )?)
295 }
296
297 pub(crate) fn into_transaction_args(
300 self,
301 tx_script: Option<TransactionScript>,
302 ) -> TransactionArgs {
303 let note_args = self.get_note_args();
304 let TransactionRequest {
305 expected_output_recipients,
306 advice_map,
307 merkle_store,
308 ..
309 } = self;
310
311 let mut tx_args = TransactionArgs::new(advice_map).with_note_args(note_args);
312
313 if let Some(tx_script) = tx_script {
317 tx_args =
318 tx_args.with_tx_script_and_args(tx_script, self.script_arg.unwrap_or_default());
319 }
320
321 if let Some(auth_argument) = self.auth_arg {
322 tx_args = tx_args.with_auth_args(auth_argument);
323 }
324
325 tx_args
326 .extend_output_note_recipients(expected_output_recipients.into_values().map(Box::new));
327 tx_args.extend_merkle_store(merkle_store.inner_nodes());
328
329 tx_args
330 }
331
332 pub(crate) fn build_transaction_script(
341 &self,
342 account_interface: &AccountInterface,
343 ) -> Result<Option<TransactionScript>, TransactionRequestError> {
344 match &self.script_template {
345 Some(TransactionScriptTemplate::CustomScript(script)) => Ok(Some(script.clone())),
346 Some(TransactionScriptTemplate::SendNotes(notes)) => {
347 Ok(Some(account_interface.build_send_notes_script(notes, self.expiration_delta)?))
350 },
351 None => Ok(None),
352 }
353 }
354}
355
356impl Serializable for TransactionRequest {
360 fn write_into<W: ByteWriter>(&self, target: &mut W) {
361 self.input_notes.write_into(target);
362 self.input_notes_args.write_into(target);
363 match &self.script_template {
364 None => target.write_u8(0),
365 Some(TransactionScriptTemplate::CustomScript(script)) => {
366 target.write_u8(1);
367 script.write_into(target);
368 },
369 Some(TransactionScriptTemplate::SendNotes(notes)) => {
370 target.write_u8(2);
371 notes.write_into(target);
372 },
373 }
374 self.expected_output_recipients.write_into(target);
375 self.expected_future_notes.write_into(target);
376 self.advice_map.write_into(target);
377 self.merkle_store.write_into(target);
378 let foreign_accounts: Vec<_> = self.foreign_accounts.values().cloned().collect();
379 foreign_accounts.write_into(target);
380 self.expiration_delta.write_into(target);
381 target.write_u8(u8::from(self.ignore_invalid_input_notes));
382 self.script_arg.write_into(target);
383 self.auth_arg.write_into(target);
384 self.expected_ntx_scripts.write_into(target);
385 }
386}
387
388impl Deserializable for TransactionRequest {
389 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
390 let input_notes = Vec::<Note>::read_from(source)?;
391 let input_notes_args = Vec::<(NoteId, Option<NoteArgs>)>::read_from(source)?;
392
393 let script_template = match source.read_u8()? {
394 0 => None,
395 1 => {
396 let transaction_script = TransactionScript::read_from(source)?;
397 Some(TransactionScriptTemplate::CustomScript(transaction_script))
398 },
399 2 => {
400 let notes = Vec::<PartialNote>::read_from(source)?;
401 Some(TransactionScriptTemplate::SendNotes(notes))
402 },
403 _ => {
404 return Err(DeserializationError::InvalidValue(
405 "Invalid script template type".to_string(),
406 ));
407 },
408 };
409
410 let expected_output_recipients = BTreeMap::<Word, NoteRecipient>::read_from(source)?;
411 let expected_future_notes =
412 BTreeMap::<NoteDetailsCommitment, (NoteDetails, NoteTag)>::read_from(source)?;
413
414 let advice_map = AdviceMap::read_from(source)?;
415 let merkle_store = MerkleStore::read_from(source)?;
416 let mut foreign_accounts = BTreeMap::new();
417 for foreign_account in Vec::<ForeignAccount>::read_from(source)? {
418 foreign_accounts.entry(foreign_account.account_id()).or_insert(foreign_account);
419 }
420 let expiration_delta = Option::<u16>::read_from(source)?;
421 let ignore_invalid_input_notes = source.read_u8()? == 1;
422 let script_arg = Option::<Word>::read_from(source)?;
423 let auth_arg = Option::<Word>::read_from(source)?;
424 let expected_ntx_scripts = Vec::<NoteScript>::read_from(source)?;
425
426 Ok(TransactionRequest {
427 input_notes,
428 input_notes_args,
429 script_template,
430 expected_output_recipients,
431 expected_future_notes,
432 advice_map,
433 merkle_store,
434 foreign_accounts,
435 expiration_delta,
436 ignore_invalid_input_notes,
437 script_arg,
438 auth_arg,
439 expected_ntx_scripts,
440 })
441 }
442}
443
444pub(crate) fn collect_assets<'a>(
449 assets: impl Iterator<Item = &'a Asset>,
450) -> (BTreeMap<AccountId, u64>, Vec<NonFungibleAsset>) {
451 let mut fungible_balance_map = BTreeMap::new();
452 let mut non_fungible_set = Vec::new();
453
454 assets.for_each(|asset| match asset {
455 Asset::Fungible(fungible) => {
456 let amount = fungible.amount().as_u64();
457 fungible_balance_map
458 .entry(fungible.faucet_id())
459 .and_modify(|balance| *balance += amount)
460 .or_insert(amount);
461 },
462 Asset::NonFungible(non_fungible) => {
463 if !non_fungible_set.contains(non_fungible) {
464 non_fungible_set.push(*non_fungible);
465 }
466 },
467 });
468
469 (fungible_balance_map, non_fungible_set)
470}
471
472impl Default for TransactionRequestBuilder {
473 fn default() -> Self {
474 Self::new()
475 }
476}
477
478#[derive(Debug, Error)]
483pub enum TransactionRequestError {
484 #[error("account interface error")]
485 AccountInterfaceError(#[from] AccountInterfaceError),
486 #[error("account error")]
487 AccountError(#[from] AccountError),
488 #[error("asset error")]
489 AssetError(#[from] AssetError),
490 #[error("duplicate input note: note {0} was added more than once to the transaction")]
491 DuplicateInputNote(NoteId),
492 #[error(
493 "the account proof does not contain the required foreign account data; re-fetch the proof and retry"
494 )]
495 ForeignAccountDataMissing,
496 #[error(
497 "foreign account {0} has incompatible visibility; use `ForeignAccount::public()` for public accounts and `ForeignAccount::private()` for private accounts"
498 )]
499 InvalidForeignAccountId(AccountId),
500 #[error(
501 "note {0} cannot be used as an authenticated input: it does not have a valid inclusion proof"
502 )]
503 InputNoteNotAuthenticated(NoteId),
504 #[error("note {0} has already been consumed")]
505 InputNoteAlreadyConsumed(NoteId),
506 #[error(
507 "output note declares sender {actual} but the transaction is executed by account {expected}"
508 )]
509 OutputNoteSenderMismatch { expected: AccountId, actual: AccountId },
510 #[error("invalid transaction script")]
511 InvalidTransactionScript(#[from] TransactionScriptError),
512 #[error("merkle proof error")]
513 MerkleError(#[from] MerkleError),
514 #[error("empty transaction: the request has no input notes and no account state changes")]
515 NoInputNotesNorAccountChange,
516 #[error("note not found: {0}")]
517 NoteNotFound(String),
518 #[error("failed to create note")]
519 NoteCreationError(#[from] NoteError),
520 #[error("note failed validation")]
521 NoteValidationError(#[source] NoteError),
522 #[error("note execution failed")]
523 NoteExecutionError(#[source] NoteError),
524 #[error("failed to build note args")]
525 NoteArgError(#[source] NoteError),
526 #[error("pay-to-ID note must contain at least one asset to transfer")]
527 P2IDNoteWithoutAsset,
528 #[error(
529 "non-fungible asset issued by faucet {0} is not available in the account vault or incoming notes"
530 )]
531 MissingNonFungibleAsset(AccountId),
532 #[error("PSWAP note can only be cancelled by its creator: expected {expected}, got {actual}")]
533 PswapCancelCreatorMismatch { expected: AccountId, actual: AccountId },
534 #[error("error building script")]
535 CodeBuilderError(#[from] CodeBuilderError),
536 #[error("transaction script template error: {0}")]
537 ScriptTemplateError(String),
538 #[error("storage slot {0} not found in account ID {1}")]
539 StorageSlotNotFound(u8, AccountId),
540 #[error("error while building the input notes")]
541 TransactionInputError(#[from] TransactionInputError),
542 #[error("account storage map error")]
543 StorageMapError(#[from] StorageMapError),
544 #[error("asset vault error")]
545 AssetVaultError(#[from] AssetVaultError),
546 #[error(
547 "unsupported authentication scheme ID {0}; supported schemes are: RpoFalcon512 (0) and EcdsaK256Keccak (1)"
548 )]
549 UnsupportedAuthSchemeId(u8),
550}
551
552#[cfg(test)]
556mod tests {
557 use std::vec::Vec;
558
559 use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment};
560 use miden_protocol::account::{
561 AccountBuilder,
562 AccountComponent,
563 AccountId,
564 AccountType,
565 StorageMapKey,
566 StorageSlotName,
567 };
568 use miden_protocol::asset::FungibleAsset;
569 use miden_protocol::crypto::rand::{FeltRng, RandomCoin};
570 use miden_protocol::note::{NoteAttachments, NoteTag, NoteType};
571 use miden_protocol::testing::account_id::{
572 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
573 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
574 ACCOUNT_ID_SENDER,
575 };
576 use miden_protocol::{EMPTY_WORD, Felt, Word};
577 use miden_standards::account::auth::AuthSingleSig;
578 use miden_standards::note::P2idNote;
579 use miden_standards::testing::account_component::MockAccountComponent;
580 use miden_tx::utils::serde::{Deserializable, Serializable};
581
582 use super::{TransactionRequest, TransactionRequestBuilder};
583 use crate::rpc::domain::account::AccountStorageRequirements;
584 use crate::transaction::ForeignAccount;
585
586 #[test]
587 fn transaction_request_serialization() {
588 assert_transaction_request_serialization_with(|| {
589 AuthSingleSig::new(
590 PublicKeyCommitment::from(EMPTY_WORD),
591 AuthScheme::Falcon512Poseidon2,
592 )
593 .into()
594 });
595 }
596
597 #[test]
598 fn transaction_request_serialization_ecdsa() {
599 assert_transaction_request_serialization_with(|| {
600 AuthSingleSig::new(PublicKeyCommitment::from(EMPTY_WORD), AuthScheme::EcdsaK256Keccak)
601 .into()
602 });
603 }
604
605 fn assert_transaction_request_serialization_with<F>(auth_component: F)
606 where
607 F: FnOnce() -> AccountComponent,
608 {
609 let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
610 let target_id =
611 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
612 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
613 let mut rng = RandomCoin::new(Word::default());
614
615 let mut notes = vec![];
616 for i in 0..6 {
617 let note = P2idNote::create(
618 sender_id,
619 target_id,
620 vec![FungibleAsset::new(faucet_id, 100 + i).unwrap().into()],
621 NoteType::Private,
622 NoteAttachments::empty(),
623 &mut rng,
624 )
625 .unwrap();
626 notes.push(note);
627 }
628
629 let mut advice_vec: Vec<(Word, Vec<Felt>)> = vec![];
630 for i in 0u32..10 {
631 advice_vec.push((rng.draw_word(), vec![Felt::from(i)]));
632 }
633
634 let account = AccountBuilder::new(Default::default())
635 .with_component(MockAccountComponent::with_empty_slots())
636 .with_auth_component(auth_component())
637 .account_type(AccountType::Private)
638 .build_existing()
639 .unwrap();
640
641 let tx_request = TransactionRequestBuilder::new()
643 .input_notes(vec![(notes.pop().unwrap(), None)])
644 .expected_output_recipients(vec![notes.pop().unwrap().recipient().clone()])
645 .expected_future_notes(vec![(
646 notes.pop().unwrap().into(),
647 NoteTag::with_account_target(sender_id),
648 )])
649 .extend_advice_map(advice_vec)
650 .foreign_accounts([
651 ForeignAccount::public(
652 target_id,
653 AccountStorageRequirements::new([(
654 StorageSlotName::new("demo::storage_slot").unwrap(),
655 &[StorageMapKey::new(Word::default())],
656 )]),
657 )
658 .unwrap(),
659 ForeignAccount::private(&account).unwrap(),
660 ])
661 .own_output_notes(vec![notes.pop().unwrap(), notes.pop().unwrap()])
662 .script_arg(rng.draw_word())
663 .auth_arg(rng.draw_word())
664 .expected_ntx_scripts(vec![notes.first().unwrap().recipient().script().clone()])
665 .build()
666 .unwrap();
667
668 let mut buffer = Vec::new();
669 tx_request.write_into(&mut buffer);
670
671 let deserialized_tx_request = TransactionRequest::read_from_bytes(&buffer).unwrap();
672 assert_eq!(tx_request, deserialized_tx_request);
673 }
674}