1use alloc::{
4 boxed::Box,
5 collections::{BTreeMap, BTreeSet},
6 string::{String, ToString},
7 vec::Vec,
8};
9
10use miden_lib::{
11 account::interface::{AccountInterface, AccountInterfaceError},
12 transaction::TransactionKernel,
13};
14use miden_objects::{
15 Digest, Felt, NoteError, TransactionInputError, TransactionScriptError, Word,
16 account::AccountId,
17 crypto::merkle::MerkleStore,
18 note::{Note, NoteDetails, NoteId, NoteRecipient, NoteTag, PartialNote},
19 transaction::{AccountInputs, InputNote, InputNotes, TransactionArgs, TransactionScript},
20 vm::AdviceMap,
21};
22use miden_tx::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable};
23use thiserror::Error;
24
25mod builder;
26pub use builder::{PaymentNoteDescription, SwapTransactionData, TransactionRequestBuilder};
27
28mod foreign;
29pub use foreign::ForeignAccount;
30
31use crate::store::InputNoteRecord;
32
33pub type NoteArgs = Word;
37
38#[derive(Clone, Debug, PartialEq, Eq)]
43pub enum TransactionScriptTemplate {
44 CustomScript(TransactionScript),
46 SendNotes(Vec<PartialNote>),
52}
53
54#[derive(Clone, Debug, PartialEq, Eq)]
60pub struct TransactionRequest {
61 unauthenticated_input_notes: Vec<Note>,
63 input_notes: Vec<(NoteId, Option<NoteArgs>)>,
66 script_template: Option<TransactionScriptTemplate>,
68 expected_output_recipients: BTreeMap<Digest, NoteRecipient>,
70 expected_future_notes: BTreeMap<NoteId, (NoteDetails, NoteTag)>,
75 advice_map: AdviceMap,
77 merkle_store: MerkleStore,
79 foreign_accounts: BTreeSet<ForeignAccount>,
83 expiration_delta: Option<u16>,
86 ignore_invalid_input_notes: bool,
90 script_arg: Option<Word>,
93}
94
95impl TransactionRequest {
96 pub fn unauthenticated_input_notes(&self) -> &[Note] {
101 &self.unauthenticated_input_notes
102 }
103
104 pub fn unauthenticated_input_note_ids(&self) -> impl Iterator<Item = NoteId> + '_ {
106 self.unauthenticated_input_notes.iter().map(Note::id)
107 }
108
109 pub fn authenticated_input_note_ids(&self) -> impl Iterator<Item = NoteId> + '_ {
111 let unauthenticated_note_ids =
112 self.unauthenticated_input_note_ids().collect::<BTreeSet<_>>();
113
114 self.input_notes().iter().filter_map(move |(note_id, _)| {
115 if unauthenticated_note_ids.contains(note_id) {
116 None
117 } else {
118 Some(*note_id)
119 }
120 })
121 }
122
123 pub fn input_notes(&self) -> &[(NoteId, Option<NoteArgs>)] {
125 &self.input_notes
126 }
127
128 pub fn get_input_note_ids(&self) -> Vec<NoteId> {
130 self.input_notes.iter().map(|(id, _)| *id).collect()
131 }
132
133 pub fn get_note_args(&self) -> BTreeMap<NoteId, NoteArgs> {
136 self.input_notes
137 .iter()
138 .filter_map(|(note, args)| args.map(|a| (*note, a)))
139 .collect()
140 }
141
142 pub fn expected_output_own_notes(&self) -> Vec<Note> {
148 match &self.script_template {
149 Some(TransactionScriptTemplate::SendNotes(notes)) => notes
150 .iter()
151 .map(|partial| {
152 Note::new(
153 partial.assets().clone(),
154 *partial.metadata(),
155 self.expected_output_recipients
156 .get(&partial.recipient_digest())
157 .expect("Recipient should be included if it's an own note")
158 .clone(),
159 )
160 })
161 .collect(),
162 _ => vec![],
163 }
164 }
165
166 pub fn expected_output_recipients(&self) -> impl Iterator<Item = &NoteRecipient> {
168 self.expected_output_recipients.values()
169 }
170
171 pub fn expected_future_notes(&self) -> impl Iterator<Item = &(NoteDetails, NoteTag)> {
173 self.expected_future_notes.values()
174 }
175
176 pub fn script_template(&self) -> &Option<TransactionScriptTemplate> {
178 &self.script_template
179 }
180
181 pub fn advice_map(&self) -> &AdviceMap {
183 &self.advice_map
184 }
185
186 pub fn merkle_store(&self) -> &MerkleStore {
188 &self.merkle_store
189 }
190
191 pub fn foreign_accounts(&self) -> &BTreeSet<ForeignAccount> {
193 &self.foreign_accounts
194 }
195
196 pub fn ignore_invalid_input_notes(&self) -> bool {
198 self.ignore_invalid_input_notes
199 }
200
201 pub(crate) fn build_input_notes(
206 &self,
207 authenticated_note_records: Vec<InputNoteRecord>,
208 ) -> Result<InputNotes<InputNote>, TransactionRequestError> {
209 let mut input_notes: BTreeMap<NoteId, InputNote> = BTreeMap::new();
210
211 for authenticated_note_record in authenticated_note_records {
213 if !authenticated_note_record.is_authenticated() {
214 return Err(TransactionRequestError::InputNoteNotAuthenticated(
215 authenticated_note_record.id(),
216 ));
217 }
218
219 if authenticated_note_record.is_consumed() {
220 return Err(TransactionRequestError::InputNoteAlreadyConsumed(
221 authenticated_note_record.id(),
222 ));
223 }
224
225 input_notes.insert(
226 authenticated_note_record.id(),
227 authenticated_note_record
228 .try_into()
229 .expect("Authenticated note record should be convertible to InputNote"),
230 );
231 }
232
233 for id in self.authenticated_input_note_ids() {
236 if !input_notes.contains_key(&id) {
237 return Err(TransactionRequestError::MissingAuthenticatedInputNote(id));
238 }
239 }
240
241 for unauthenticated_input_notes in &self.unauthenticated_input_notes {
243 input_notes.insert(
244 unauthenticated_input_notes.id(),
245 InputNote::Unauthenticated {
246 note: unauthenticated_input_notes.clone(),
247 },
248 );
249 }
250
251 Ok(InputNotes::new(
252 self.get_input_note_ids()
253 .iter()
254 .map(|note_id| {
255 input_notes
256 .remove(note_id)
257 .expect("The input note map was checked to contain all input notes")
258 })
259 .collect(),
260 )?)
261 }
262
263 pub(crate) fn into_transaction_args(
266 self,
267 tx_script: TransactionScript,
268 foreign_account_inputs: Vec<AccountInputs>,
269 ) -> TransactionArgs {
270 let note_args = self.get_note_args();
271 let TransactionRequest {
272 expected_output_recipients,
273 advice_map,
274 merkle_store,
275 ..
276 } = self;
277
278 let mut tx_args =
279 TransactionArgs::new(advice_map, foreign_account_inputs).with_note_args(note_args);
280
281 tx_args = if let Some(argument) = self.script_arg {
282 tx_args.with_tx_script_and_arg(tx_script, argument)
283 } else {
284 tx_args.with_tx_script(tx_script)
285 };
286
287 tx_args
288 .extend_output_note_recipients(expected_output_recipients.into_values().map(Box::new));
289 tx_args.extend_merkle_store(merkle_store.inner_nodes());
290
291 tx_args
292 }
293
294 pub(crate) fn build_transaction_script(
297 &self,
298 account_interface: &AccountInterface,
299 in_debug_mode: bool,
300 ) -> Result<TransactionScript, TransactionRequestError> {
301 match &self.script_template {
302 Some(TransactionScriptTemplate::CustomScript(script)) => Ok(script.clone()),
303 Some(TransactionScriptTemplate::SendNotes(notes)) => Ok(account_interface
304 .build_send_notes_script(notes, self.expiration_delta, in_debug_mode)?),
305 None => {
306 if self.input_notes.is_empty() {
307 return Err(TransactionRequestError::NoInputNotes);
308 }
309
310 let empty_script =
311 TransactionScript::compile("begin nop end", TransactionKernel::assembler())?;
312
313 Ok(empty_script)
314 },
315 }
316 }
317}
318
319impl Serializable for TransactionRequest {
323 fn write_into<W: ByteWriter>(&self, target: &mut W) {
324 self.unauthenticated_input_notes.write_into(target);
325 self.input_notes.write_into(target);
326 match &self.script_template {
327 None => target.write_u8(0),
328 Some(TransactionScriptTemplate::CustomScript(script)) => {
329 target.write_u8(1);
330 script.write_into(target);
331 },
332 Some(TransactionScriptTemplate::SendNotes(notes)) => {
333 target.write_u8(2);
334 notes.write_into(target);
335 },
336 }
337 self.expected_output_recipients.write_into(target);
338 self.expected_future_notes.write_into(target);
339 self.advice_map.clone().into_iter().collect::<Vec<_>>().write_into(target);
340 self.merkle_store.write_into(target);
341 self.foreign_accounts.write_into(target);
342 self.expiration_delta.write_into(target);
343 target.write_u8(u8::from(self.ignore_invalid_input_notes));
344 self.script_arg.write_into(target);
345 }
346}
347
348impl Deserializable for TransactionRequest {
349 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
350 let unauthenticated_input_notes = Vec::<Note>::read_from(source)?;
351 let input_notes = Vec::<(NoteId, Option<NoteArgs>)>::read_from(source)?;
352
353 let script_template = match source.read_u8()? {
354 0 => None,
355 1 => {
356 let transaction_script = TransactionScript::read_from(source)?;
357 Some(TransactionScriptTemplate::CustomScript(transaction_script))
358 },
359 2 => {
360 let notes = Vec::<PartialNote>::read_from(source)?;
361 Some(TransactionScriptTemplate::SendNotes(notes))
362 },
363 _ => {
364 return Err(DeserializationError::InvalidValue(
365 "Invalid script template type".to_string(),
366 ));
367 },
368 };
369
370 let expected_output_recipients = BTreeMap::<Digest, NoteRecipient>::read_from(source)?;
371 let expected_future_notes = BTreeMap::<NoteId, (NoteDetails, NoteTag)>::read_from(source)?;
372
373 let mut advice_map = AdviceMap::new();
374 let advice_vec = Vec::<(Digest, Vec<Felt>)>::read_from(source)?;
375 advice_map.extend(advice_vec);
376 let merkle_store = MerkleStore::read_from(source)?;
377 let foreign_accounts = BTreeSet::<ForeignAccount>::read_from(source)?;
378 let expiration_delta = Option::<u16>::read_from(source)?;
379 let ignore_invalid_input_notes = source.read_u8()? == 1;
380 let script_arg = Option::<Word>::read_from(source)?;
381
382 Ok(TransactionRequest {
383 unauthenticated_input_notes,
384 input_notes,
385 script_template,
386 expected_output_recipients,
387 expected_future_notes,
388 advice_map,
389 merkle_store,
390 foreign_accounts,
391 expiration_delta,
392 ignore_invalid_input_notes,
393 script_arg,
394 })
395 }
396}
397
398impl Default for TransactionRequestBuilder {
399 fn default() -> Self {
400 Self::new()
401 }
402}
403
404#[derive(Debug, Error)]
409pub enum TransactionRequestError {
410 #[error("account interface error")]
411 AccountInterfaceError(#[from] AccountInterfaceError),
412 #[error("duplicate input note with IDs: {0}")]
413 DuplicateInputNote(NoteId),
414 #[error("foreign account data missing in the account proof")]
415 ForeignAccountDataMissing,
416 #[error("foreign account storage slot {0} is not a map type")]
417 ForeignAccountStorageSlotInvalidIndex(u8),
418 #[error("requested foreign account with ID {0} does not have an expected storage mode")]
419 InvalidForeignAccountId(AccountId),
420 #[error("note {0} does not contain a valid inclusion proof")]
421 InputNoteNotAuthenticated(NoteId),
422 #[error("note {0} has already been consumed")]
423 InputNoteAlreadyConsumed(NoteId),
424 #[error("own notes shouldn't be of the header variant")]
425 InvalidNoteVariant,
426 #[error("invalid sender account id: {0}")]
427 InvalidSenderAccount(AccountId),
428 #[error("invalid transaction script")]
429 InvalidTransactionScript(#[from] TransactionScriptError),
430 #[error("specified authenticated input note with id {0} is missing")]
431 MissingAuthenticatedInputNote(NoteId),
432 #[error("a transaction without output notes must have at least one input note")]
433 NoInputNotes,
434 #[error("note not found: {0}")]
435 NoteNotFound(String),
436 #[error("note creation error")]
437 NoteCreationError(#[from] NoteError),
438 #[error("pay to id note doesn't contain at least one asset")]
439 P2IDNoteWithoutAsset,
440 #[error("transaction script template error: {0}")]
441 ScriptTemplateError(String),
442 #[error("storage slot {0} not found in account ID {1}")]
443 StorageSlotNotFound(u8, AccountId),
444 #[error("error while building the input notes: {0}")]
445 TransactionInputError(#[from] TransactionInputError),
446}
447
448#[cfg(test)]
452mod tests {
453 use std::vec::Vec;
454
455 use miden_lib::{
456 account::auth::RpoFalcon512, note::create_p2id_note, transaction::TransactionKernel,
457 };
458 use miden_objects::{
459 Digest, EMPTY_WORD, Felt, ZERO,
460 account::{AccountBuilder, AccountId, AccountType},
461 asset::FungibleAsset,
462 crypto::{
463 dsa::rpo_falcon512::PublicKey,
464 rand::{FeltRng, RpoRandomCoin},
465 },
466 note::{NoteTag, NoteType},
467 testing::{
468 account_component::AccountMockComponent,
469 account_id::{
470 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
471 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, ACCOUNT_ID_SENDER,
472 },
473 },
474 transaction::OutputNote,
475 };
476 use miden_tx::utils::{Deserializable, Serializable};
477
478 use super::{TransactionRequest, TransactionRequestBuilder};
479 use crate::{rpc::domain::account::AccountStorageRequirements, transaction::ForeignAccount};
480
481 #[test]
482 fn transaction_request_serialization() {
483 let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
484 let target_id =
485 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
486 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
487 let mut rng = RpoRandomCoin::new(Default::default());
488
489 let mut notes = vec![];
490 for i in 0..6 {
491 let note = create_p2id_note(
492 sender_id,
493 target_id,
494 vec![FungibleAsset::new(faucet_id, 100 + i).unwrap().into()],
495 NoteType::Private,
496 ZERO,
497 &mut rng,
498 )
499 .unwrap();
500 notes.push(note);
501 }
502
503 let mut advice_vec: Vec<(Digest, Vec<Felt>)> = vec![];
504 for i in 0..10 {
505 advice_vec.push((Digest::new(rng.draw_word()), vec![Felt::new(i)]));
506 }
507
508 let account = AccountBuilder::new(Default::default())
509 .with_component(
510 AccountMockComponent::new_with_empty_slots(TransactionKernel::assembler()).unwrap(),
511 )
512 .with_auth_component(RpoFalcon512::new(PublicKey::new(EMPTY_WORD)))
513 .account_type(AccountType::RegularAccountImmutableCode)
514 .storage_mode(miden_objects::account::AccountStorageMode::Private)
515 .build_existing()
516 .unwrap();
517
518 let tx_request = TransactionRequestBuilder::new()
520 .authenticated_input_notes(vec![(notes.pop().unwrap().id(), None)])
521 .unauthenticated_input_notes(vec![(notes.pop().unwrap(), None)])
522 .expected_output_recipients(vec![notes.pop().unwrap().recipient().clone()])
523 .expected_future_notes(vec![(
524 notes.pop().unwrap().into(),
525 NoteTag::from_account_id(sender_id),
526 )])
527 .extend_advice_map(advice_vec)
528 .foreign_accounts([
529 ForeignAccount::public(
530 target_id,
531 AccountStorageRequirements::new([(5u8, &[Digest::default()])]),
532 )
533 .unwrap(),
534 ForeignAccount::private(account).unwrap(),
535 ])
536 .own_output_notes(vec![
537 OutputNote::Full(notes.pop().unwrap()),
538 OutputNote::Partial(notes.pop().unwrap().into()),
539 ])
540 .build()
541 .unwrap();
542
543 let mut buffer = Vec::new();
544 tx_request.write_into(&mut buffer);
545
546 let deserialized_tx_request = TransactionRequest::read_from_bytes(&buffer).unwrap();
547 assert_eq!(tx_request, deserialized_tx_request);
548 }
549}