1use alloc::boxed::Box;
4use alloc::collections::{BTreeMap, BTreeSet};
5use alloc::string::{String, ToString};
6use alloc::vec::Vec;
7
8use miden_lib::account::interface::{AccountInterface, AccountInterfaceError};
9use miden_lib::utils::{ScriptBuilder, ScriptBuilderError};
10use miden_objects::account::AccountId;
11use miden_objects::crypto::merkle::{MerkleError, MerkleStore};
12use miden_objects::note::{Note, NoteDetails, NoteId, NoteRecipient, NoteTag, PartialNote};
13use miden_objects::transaction::{InputNote, InputNotes, TransactionArgs, TransactionScript};
14use miden_objects::vm::AdviceMap;
15use miden_objects::{
16 AccountError,
17 AssetVaultError,
18 NoteError,
19 StorageMapError,
20 TransactionInputError,
21 TransactionScriptError,
22 Word,
23};
24use miden_tx::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable};
25use thiserror::Error;
26
27mod builder;
28pub use builder::{PaymentNoteDescription, SwapTransactionData, TransactionRequestBuilder};
29
30mod foreign;
31pub use foreign::ForeignAccount;
32
33use crate::DebugMode;
34use crate::store::InputNoteRecord;
35
36pub type NoteArgs = Word;
40
41#[derive(Clone, Debug, PartialEq, Eq)]
46pub enum TransactionScriptTemplate {
47 CustomScript(TransactionScript),
49 SendNotes(Vec<PartialNote>),
55}
56
57#[derive(Clone, Debug, PartialEq, Eq)]
63pub struct TransactionRequest {
64 unauthenticated_input_notes: Vec<Note>,
66 input_notes: Vec<(NoteId, Option<NoteArgs>)>,
69 script_template: Option<TransactionScriptTemplate>,
71 expected_output_recipients: BTreeMap<Word, NoteRecipient>,
73 expected_future_notes: BTreeMap<NoteId, (NoteDetails, NoteTag)>,
78 advice_map: AdviceMap,
80 merkle_store: MerkleStore,
82 foreign_accounts: BTreeSet<ForeignAccount>,
86 expiration_delta: Option<u16>,
89 ignore_invalid_input_notes: bool,
93 script_arg: Option<Word>,
96 auth_arg: Option<Word>,
99}
100
101impl TransactionRequest {
102 pub fn unauthenticated_input_notes(&self) -> &[Note] {
107 &self.unauthenticated_input_notes
108 }
109
110 pub fn unauthenticated_input_note_ids(&self) -> impl Iterator<Item = NoteId> + '_ {
112 self.unauthenticated_input_notes.iter().map(Note::id)
113 }
114
115 pub fn authenticated_input_note_ids(&self) -> impl Iterator<Item = NoteId> + '_ {
117 let unauthenticated_note_ids =
118 self.unauthenticated_input_note_ids().collect::<BTreeSet<_>>();
119
120 self.input_notes().iter().filter_map(move |(note_id, _)| {
121 if unauthenticated_note_ids.contains(note_id) {
122 None
123 } else {
124 Some(*note_id)
125 }
126 })
127 }
128
129 pub fn input_notes(&self) -> &[(NoteId, Option<NoteArgs>)] {
131 &self.input_notes
132 }
133
134 pub fn get_input_note_ids(&self) -> Vec<NoteId> {
136 self.input_notes.iter().map(|(id, _)| *id).collect()
137 }
138
139 pub fn get_note_args(&self) -> BTreeMap<NoteId, NoteArgs> {
142 self.input_notes
143 .iter()
144 .filter_map(|(note, args)| args.map(|a| (*note, a)))
145 .collect()
146 }
147
148 pub fn expected_output_own_notes(&self) -> Vec<Note> {
154 match &self.script_template {
155 Some(TransactionScriptTemplate::SendNotes(notes)) => notes
156 .iter()
157 .map(|partial| {
158 Note::new(
159 partial.assets().clone(),
160 *partial.metadata(),
161 self.expected_output_recipients
162 .get(&partial.recipient_digest())
163 .expect("Recipient should be included if it's an own note")
164 .clone(),
165 )
166 })
167 .collect(),
168 _ => vec![],
169 }
170 }
171
172 pub fn expected_output_recipients(&self) -> impl Iterator<Item = &NoteRecipient> {
174 self.expected_output_recipients.values()
175 }
176
177 pub fn expected_future_notes(&self) -> impl Iterator<Item = &(NoteDetails, NoteTag)> {
179 self.expected_future_notes.values()
180 }
181
182 pub fn script_template(&self) -> &Option<TransactionScriptTemplate> {
184 &self.script_template
185 }
186
187 pub fn advice_map(&self) -> &AdviceMap {
189 &self.advice_map
190 }
191
192 pub fn advice_map_mut(&mut self) -> &mut AdviceMap {
194 &mut self.advice_map
195 }
196
197 pub fn merkle_store(&self) -> &MerkleStore {
199 &self.merkle_store
200 }
201
202 pub fn foreign_accounts(&self) -> &BTreeSet<ForeignAccount> {
204 &self.foreign_accounts
205 }
206
207 pub fn ignore_invalid_input_notes(&self) -> bool {
209 self.ignore_invalid_input_notes
210 }
211
212 pub fn script_arg(&self) -> &Option<Word> {
214 &self.script_arg
215 }
216
217 pub fn auth_arg(&self) -> &Option<Word> {
219 &self.auth_arg
220 }
221
222 pub(crate) fn build_input_notes(
227 &self,
228 authenticated_note_records: Vec<InputNoteRecord>,
229 ) -> Result<InputNotes<InputNote>, TransactionRequestError> {
230 let mut input_notes: BTreeMap<NoteId, InputNote> = BTreeMap::new();
231
232 for authenticated_note_record in authenticated_note_records {
234 if !authenticated_note_record.is_authenticated() {
235 return Err(TransactionRequestError::InputNoteNotAuthenticated(
236 authenticated_note_record.id(),
237 ));
238 }
239
240 if authenticated_note_record.is_consumed() {
241 return Err(TransactionRequestError::InputNoteAlreadyConsumed(
242 authenticated_note_record.id(),
243 ));
244 }
245
246 input_notes.insert(
247 authenticated_note_record.id(),
248 authenticated_note_record
249 .try_into()
250 .expect("Authenticated note record should be convertible to InputNote"),
251 );
252 }
253
254 for id in self.authenticated_input_note_ids() {
257 if !input_notes.contains_key(&id) {
258 return Err(TransactionRequestError::MissingAuthenticatedInputNote(id));
259 }
260 }
261
262 for unauthenticated_input_notes in &self.unauthenticated_input_notes {
264 input_notes.insert(
265 unauthenticated_input_notes.id(),
266 InputNote::Unauthenticated {
267 note: unauthenticated_input_notes.clone(),
268 },
269 );
270 }
271
272 Ok(InputNotes::new(
273 self.get_input_note_ids()
274 .iter()
275 .map(|note_id| {
276 input_notes
277 .remove(note_id)
278 .expect("The input note map was checked to contain all input notes")
279 })
280 .collect(),
281 )?)
282 }
283
284 pub(crate) fn into_transaction_args(self, tx_script: TransactionScript) -> TransactionArgs {
287 let note_args = self.get_note_args();
288 let TransactionRequest {
289 expected_output_recipients,
290 advice_map,
291 merkle_store,
292 ..
293 } = self;
294
295 let mut tx_args = TransactionArgs::new(advice_map).with_note_args(note_args);
296
297 tx_args = if let Some(argument) = self.script_arg {
298 tx_args.with_tx_script_and_args(tx_script, argument)
299 } else {
300 tx_args.with_tx_script(tx_script)
301 };
302
303 if let Some(auth_argument) = self.auth_arg {
304 tx_args = tx_args.with_auth_args(auth_argument);
305 }
306
307 tx_args
308 .extend_output_note_recipients(expected_output_recipients.into_values().map(Box::new));
309 tx_args.extend_merkle_store(merkle_store.inner_nodes());
310
311 tx_args
312 }
313
314 pub(crate) fn build_transaction_script(
317 &self,
318 account_interface: &AccountInterface,
319 in_debug_mode: DebugMode,
320 ) -> Result<TransactionScript, TransactionRequestError> {
321 match &self.script_template {
322 Some(TransactionScriptTemplate::CustomScript(script)) => Ok(script.clone()),
323 Some(TransactionScriptTemplate::SendNotes(notes)) => Ok(account_interface
324 .build_send_notes_script(notes, self.expiration_delta, in_debug_mode.into())?),
325 None => {
326 let empty_script = ScriptBuilder::new(true).compile_tx_script("begin nop end")?;
327
328 Ok(empty_script)
329 },
330 }
331 }
332}
333
334impl Serializable for TransactionRequest {
338 fn write_into<W: ByteWriter>(&self, target: &mut W) {
339 self.unauthenticated_input_notes.write_into(target);
340 self.input_notes.write_into(target);
341 match &self.script_template {
342 None => target.write_u8(0),
343 Some(TransactionScriptTemplate::CustomScript(script)) => {
344 target.write_u8(1);
345 script.write_into(target);
346 },
347 Some(TransactionScriptTemplate::SendNotes(notes)) => {
348 target.write_u8(2);
349 notes.write_into(target);
350 },
351 }
352 self.expected_output_recipients.write_into(target);
353 self.expected_future_notes.write_into(target);
354 self.advice_map.write_into(target);
355 self.merkle_store.write_into(target);
356 self.foreign_accounts.write_into(target);
357 self.expiration_delta.write_into(target);
358 target.write_u8(u8::from(self.ignore_invalid_input_notes));
359 self.script_arg.write_into(target);
360 self.auth_arg.write_into(target);
361 }
362}
363
364impl Deserializable for TransactionRequest {
365 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
366 let unauthenticated_input_notes = Vec::<Note>::read_from(source)?;
367 let input_notes = Vec::<(NoteId, Option<NoteArgs>)>::read_from(source)?;
368
369 let script_template = match source.read_u8()? {
370 0 => None,
371 1 => {
372 let transaction_script = TransactionScript::read_from(source)?;
373 Some(TransactionScriptTemplate::CustomScript(transaction_script))
374 },
375 2 => {
376 let notes = Vec::<PartialNote>::read_from(source)?;
377 Some(TransactionScriptTemplate::SendNotes(notes))
378 },
379 _ => {
380 return Err(DeserializationError::InvalidValue(
381 "Invalid script template type".to_string(),
382 ));
383 },
384 };
385
386 let expected_output_recipients = BTreeMap::<Word, NoteRecipient>::read_from(source)?;
387 let expected_future_notes = BTreeMap::<NoteId, (NoteDetails, NoteTag)>::read_from(source)?;
388
389 let advice_map = AdviceMap::read_from(source)?;
390 let merkle_store = MerkleStore::read_from(source)?;
391 let foreign_accounts = BTreeSet::<ForeignAccount>::read_from(source)?;
392 let expiration_delta = Option::<u16>::read_from(source)?;
393 let ignore_invalid_input_notes = source.read_u8()? == 1;
394 let script_arg = Option::<Word>::read_from(source)?;
395 let auth_arg = Option::<Word>::read_from(source)?;
396
397 Ok(TransactionRequest {
398 unauthenticated_input_notes,
399 input_notes,
400 script_template,
401 expected_output_recipients,
402 expected_future_notes,
403 advice_map,
404 merkle_store,
405 foreign_accounts,
406 expiration_delta,
407 ignore_invalid_input_notes,
408 script_arg,
409 auth_arg,
410 })
411 }
412}
413
414impl Default for TransactionRequestBuilder {
415 fn default() -> Self {
416 Self::new()
417 }
418}
419
420#[derive(Debug, Error)]
425pub enum TransactionRequestError {
426 #[error("account interface error")]
427 AccountInterfaceError(#[from] AccountInterfaceError),
428 #[error("account error")]
429 AccountError(#[from] AccountError),
430 #[error("duplicate input note with IDs: {0}")]
431 DuplicateInputNote(NoteId),
432 #[error("foreign account data missing in the account proof")]
433 ForeignAccountDataMissing,
434 #[error("foreign account storage slot {0} is not a map type")]
435 ForeignAccountStorageSlotInvalidIndex(u8),
436 #[error("requested foreign account with ID {0} does not have an expected storage mode")]
437 InvalidForeignAccountId(AccountId),
438 #[error("note {0} does not contain a valid inclusion proof")]
439 InputNoteNotAuthenticated(NoteId),
440 #[error("note {0} has already been consumed")]
441 InputNoteAlreadyConsumed(NoteId),
442 #[error("own notes shouldn't be of the header variant")]
443 InvalidNoteVariant,
444 #[error("invalid sender account id: {0}")]
445 InvalidSenderAccount(AccountId),
446 #[error("invalid transaction script")]
447 InvalidTransactionScript(#[from] TransactionScriptError),
448 #[error("merkle error")]
449 MerkleError(#[from] MerkleError),
450 #[error("specified authenticated input note with id {0} is missing")]
451 MissingAuthenticatedInputNote(NoteId),
452 #[error("a transaction without output notes must have at least one input note")]
453 NoInputNotesNorAccountChange,
454 #[error("note not found: {0}")]
455 NoteNotFound(String),
456 #[error("note creation error")]
457 NoteCreationError(#[from] NoteError),
458 #[error("pay to id note doesn't contain at least one asset")]
459 P2IDNoteWithoutAsset,
460 #[error("error building script: {0}")]
461 ScriptBuilderError(#[from] ScriptBuilderError),
462 #[error("transaction script template error: {0}")]
463 ScriptTemplateError(String),
464 #[error("storage slot {0} not found in account ID {1}")]
465 StorageSlotNotFound(u8, AccountId),
466 #[error("error while building the input notes: {0}")]
467 TransactionInputError(#[from] TransactionInputError),
468 #[error("storage map error")]
469 StorageMapError(#[from] StorageMapError),
470 #[error("asset vault error")]
471 AssetVaultError(#[from] AssetVaultError),
472 #[error("unsupported authentication scheme ID: {0}")]
473 UnsupportedAuthSchemeId(u8),
474}
475
476#[cfg(test)]
480mod tests {
481 use std::vec::Vec;
482
483 use miden_lib::account::auth::{AuthEcdsaK256Keccak, AuthRpoFalcon512};
484 use miden_lib::note::create_p2id_note;
485 use miden_lib::testing::account_component::MockAccountComponent;
486 use miden_objects::account::auth::PublicKeyCommitment;
487 use miden_objects::account::{AccountBuilder, AccountId, AccountType};
488 use miden_objects::asset::FungibleAsset;
489 use miden_objects::crypto::rand::{FeltRng, RpoRandomCoin};
490 use miden_objects::note::{NoteTag, NoteType};
491 use miden_objects::testing::account_id::{
492 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
493 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
494 ACCOUNT_ID_SENDER,
495 };
496 use miden_objects::transaction::OutputNote;
497 use miden_objects::{EMPTY_WORD, Felt, Word, ZERO};
498 use miden_tx::utils::{Deserializable, Serializable};
499
500 use super::{TransactionRequest, TransactionRequestBuilder};
501 use crate::rpc::domain::account::AccountStorageRequirements;
502 use crate::transaction::ForeignAccount;
503
504 #[test]
505 fn transaction_request_serialization() {
506 let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
507 let target_id =
508 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
509 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
510 let mut rng = RpoRandomCoin::new(Word::default());
511
512 let mut notes = vec![];
513 for i in 0..6 {
514 let note = create_p2id_note(
515 sender_id,
516 target_id,
517 vec![FungibleAsset::new(faucet_id, 100 + i).unwrap().into()],
518 NoteType::Private,
519 ZERO,
520 &mut rng,
521 )
522 .unwrap();
523 notes.push(note);
524 }
525
526 let mut advice_vec: Vec<(Word, Vec<Felt>)> = vec![];
527 for i in 0..10 {
528 advice_vec.push((rng.draw_word(), vec![Felt::new(i)]));
529 }
530
531 let account = AccountBuilder::new(Default::default())
532 .with_component(MockAccountComponent::with_empty_slots())
533 .with_auth_component(AuthRpoFalcon512::new(PublicKeyCommitment::from(EMPTY_WORD)))
534 .account_type(AccountType::RegularAccountImmutableCode)
535 .storage_mode(miden_objects::account::AccountStorageMode::Private)
536 .build_existing()
537 .unwrap();
538
539 let tx_request = TransactionRequestBuilder::new()
541 .authenticated_input_notes(vec![(notes.pop().unwrap().id(), None)])
542 .unauthenticated_input_notes(vec![(notes.pop().unwrap(), None)])
543 .expected_output_recipients(vec![notes.pop().unwrap().recipient().clone()])
544 .expected_future_notes(vec![(
545 notes.pop().unwrap().into(),
546 NoteTag::from_account_id(sender_id),
547 )])
548 .extend_advice_map(advice_vec)
549 .foreign_accounts([
550 ForeignAccount::public(
551 target_id,
552 AccountStorageRequirements::new([(5u8, &[Word::default()])]),
553 )
554 .unwrap(),
555 ForeignAccount::private(&account).unwrap(),
556 ])
557 .own_output_notes(vec![
558 OutputNote::Full(notes.pop().unwrap()),
559 OutputNote::Partial(notes.pop().unwrap().into()),
560 ])
561 .script_arg(rng.draw_word())
562 .auth_arg(rng.draw_word())
563 .build()
564 .unwrap();
565
566 let mut buffer = Vec::new();
567 tx_request.write_into(&mut buffer);
568
569 let deserialized_tx_request = TransactionRequest::read_from_bytes(&buffer).unwrap();
570 assert_eq!(tx_request, deserialized_tx_request);
571 }
572
573 #[test]
574 fn transaction_request_serialization_ecdsa() {
575 let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
576 let target_id =
577 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
578 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
579 let mut rng = RpoRandomCoin::new(Word::default());
580
581 let mut notes = vec![];
582 for i in 0..6 {
583 let note = create_p2id_note(
584 sender_id,
585 target_id,
586 vec![FungibleAsset::new(faucet_id, 100 + i).unwrap().into()],
587 NoteType::Private,
588 ZERO,
589 &mut rng,
590 )
591 .unwrap();
592 notes.push(note);
593 }
594
595 let mut advice_vec: Vec<(Word, Vec<Felt>)> = vec![];
596 for i in 0..10 {
597 advice_vec.push((rng.draw_word(), vec![Felt::new(i)]));
598 }
599
600 let account = AccountBuilder::new(Default::default())
601 .with_component(MockAccountComponent::with_empty_slots())
602 .with_auth_component(AuthEcdsaK256Keccak::new(PublicKeyCommitment::from(EMPTY_WORD)))
603 .account_type(AccountType::RegularAccountImmutableCode)
604 .storage_mode(miden_objects::account::AccountStorageMode::Private)
605 .build_existing()
606 .unwrap();
607
608 let tx_request = TransactionRequestBuilder::new()
610 .authenticated_input_notes(vec![(notes.pop().unwrap().id(), None)])
611 .unauthenticated_input_notes(vec![(notes.pop().unwrap(), None)])
612 .expected_output_recipients(vec![notes.pop().unwrap().recipient().clone()])
613 .expected_future_notes(vec![(
614 notes.pop().unwrap().into(),
615 NoteTag::from_account_id(sender_id),
616 )])
617 .extend_advice_map(advice_vec)
618 .foreign_accounts([
619 ForeignAccount::public(
620 target_id,
621 AccountStorageRequirements::new([(5u8, &[Word::default()])]),
622 )
623 .unwrap(),
624 ForeignAccount::private(&account).unwrap(),
625 ])
626 .own_output_notes(vec![
627 OutputNote::Full(notes.pop().unwrap()),
628 OutputNote::Partial(notes.pop().unwrap().into()),
629 ])
630 .script_arg(rng.draw_word())
631 .auth_arg(rng.draw_word())
632 .build()
633 .unwrap();
634
635 let mut buffer = Vec::new();
636 tx_request.write_into(&mut buffer);
637
638 let deserialized_tx_request = TransactionRequest::read_from_bytes(&buffer).unwrap();
639 assert_eq!(tx_request, deserialized_tx_request);
640 }
641}