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::{
14 AccountInputs,
15 InputNote,
16 InputNotes,
17 TransactionArgs,
18 TransactionScript,
19};
20use miden_objects::vm::AdviceMap;
21use miden_objects::{AccountError, NoteError, TransactionInputError, TransactionScriptError, Word};
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::DebugMode;
32use crate::store::InputNoteRecord;
33
34pub type NoteArgs = Word;
38
39#[derive(Clone, Debug, PartialEq, Eq)]
44pub enum TransactionScriptTemplate {
45 CustomScript(TransactionScript),
47 SendNotes(Vec<PartialNote>),
53}
54
55#[derive(Clone, Debug, PartialEq, Eq)]
61pub struct TransactionRequest {
62 unauthenticated_input_notes: Vec<Note>,
64 input_notes: Vec<(NoteId, Option<NoteArgs>)>,
67 script_template: Option<TransactionScriptTemplate>,
69 expected_output_recipients: BTreeMap<Word, NoteRecipient>,
71 expected_future_notes: BTreeMap<NoteId, (NoteDetails, NoteTag)>,
76 advice_map: AdviceMap,
78 merkle_store: MerkleStore,
80 foreign_accounts: BTreeSet<ForeignAccount>,
84 expiration_delta: Option<u16>,
87 ignore_invalid_input_notes: bool,
91 script_arg: Option<Word>,
94 auth_arg: Option<Word>,
97}
98
99impl TransactionRequest {
100 pub fn unauthenticated_input_notes(&self) -> &[Note] {
105 &self.unauthenticated_input_notes
106 }
107
108 pub fn unauthenticated_input_note_ids(&self) -> impl Iterator<Item = NoteId> + '_ {
110 self.unauthenticated_input_notes.iter().map(Note::id)
111 }
112
113 pub fn authenticated_input_note_ids(&self) -> impl Iterator<Item = NoteId> + '_ {
115 let unauthenticated_note_ids =
116 self.unauthenticated_input_note_ids().collect::<BTreeSet<_>>();
117
118 self.input_notes().iter().filter_map(move |(note_id, _)| {
119 if unauthenticated_note_ids.contains(note_id) {
120 None
121 } else {
122 Some(*note_id)
123 }
124 })
125 }
126
127 pub fn input_notes(&self) -> &[(NoteId, Option<NoteArgs>)] {
129 &self.input_notes
130 }
131
132 pub fn get_input_note_ids(&self) -> Vec<NoteId> {
134 self.input_notes.iter().map(|(id, _)| *id).collect()
135 }
136
137 pub fn get_note_args(&self) -> BTreeMap<NoteId, NoteArgs> {
140 self.input_notes
141 .iter()
142 .filter_map(|(note, args)| args.map(|a| (*note, a)))
143 .collect()
144 }
145
146 pub fn expected_output_own_notes(&self) -> Vec<Note> {
152 match &self.script_template {
153 Some(TransactionScriptTemplate::SendNotes(notes)) => notes
154 .iter()
155 .map(|partial| {
156 Note::new(
157 partial.assets().clone(),
158 *partial.metadata(),
159 self.expected_output_recipients
160 .get(&partial.recipient_digest())
161 .expect("Recipient should be included if it's an own note")
162 .clone(),
163 )
164 })
165 .collect(),
166 _ => vec![],
167 }
168 }
169
170 pub fn expected_output_recipients(&self) -> impl Iterator<Item = &NoteRecipient> {
172 self.expected_output_recipients.values()
173 }
174
175 pub fn expected_future_notes(&self) -> impl Iterator<Item = &(NoteDetails, NoteTag)> {
177 self.expected_future_notes.values()
178 }
179
180 pub fn script_template(&self) -> &Option<TransactionScriptTemplate> {
182 &self.script_template
183 }
184
185 pub fn advice_map(&self) -> &AdviceMap {
187 &self.advice_map
188 }
189
190 pub fn advice_map_mut(&mut self) -> &mut AdviceMap {
192 &mut self.advice_map
193 }
194
195 pub fn merkle_store(&self) -> &MerkleStore {
197 &self.merkle_store
198 }
199
200 pub fn foreign_accounts(&self) -> &BTreeSet<ForeignAccount> {
202 &self.foreign_accounts
203 }
204
205 pub fn ignore_invalid_input_notes(&self) -> bool {
207 self.ignore_invalid_input_notes
208 }
209
210 pub fn script_arg(&self) -> &Option<Word> {
212 &self.script_arg
213 }
214
215 pub fn auth_arg(&self) -> &Option<Word> {
217 &self.auth_arg
218 }
219
220 pub(crate) fn build_input_notes(
225 &self,
226 authenticated_note_records: Vec<InputNoteRecord>,
227 ) -> Result<InputNotes<InputNote>, TransactionRequestError> {
228 let mut input_notes: BTreeMap<NoteId, InputNote> = BTreeMap::new();
229
230 for authenticated_note_record in authenticated_note_records {
232 if !authenticated_note_record.is_authenticated() {
233 return Err(TransactionRequestError::InputNoteNotAuthenticated(
234 authenticated_note_record.id(),
235 ));
236 }
237
238 if authenticated_note_record.is_consumed() {
239 return Err(TransactionRequestError::InputNoteAlreadyConsumed(
240 authenticated_note_record.id(),
241 ));
242 }
243
244 input_notes.insert(
245 authenticated_note_record.id(),
246 authenticated_note_record
247 .try_into()
248 .expect("Authenticated note record should be convertible to InputNote"),
249 );
250 }
251
252 for id in self.authenticated_input_note_ids() {
255 if !input_notes.contains_key(&id) {
256 return Err(TransactionRequestError::MissingAuthenticatedInputNote(id));
257 }
258 }
259
260 for unauthenticated_input_notes in &self.unauthenticated_input_notes {
262 input_notes.insert(
263 unauthenticated_input_notes.id(),
264 InputNote::Unauthenticated {
265 note: unauthenticated_input_notes.clone(),
266 },
267 );
268 }
269
270 Ok(InputNotes::new(
271 self.get_input_note_ids()
272 .iter()
273 .map(|note_id| {
274 input_notes
275 .remove(note_id)
276 .expect("The input note map was checked to contain all input notes")
277 })
278 .collect(),
279 )?)
280 }
281
282 pub(crate) fn into_transaction_args(
285 self,
286 tx_script: TransactionScript,
287 foreign_account_inputs: Vec<AccountInputs>,
288 ) -> TransactionArgs {
289 let note_args = self.get_note_args();
290 let TransactionRequest {
291 expected_output_recipients,
292 advice_map,
293 merkle_store,
294 ..
295 } = self;
296
297 let mut tx_args =
298 TransactionArgs::new(advice_map, foreign_account_inputs).with_note_args(note_args);
299
300 tx_args = if let Some(argument) = self.script_arg {
301 tx_args.with_tx_script_and_args(tx_script, argument)
302 } else {
303 tx_args.with_tx_script(tx_script)
304 };
305
306 if let Some(auth_argument) = self.auth_arg {
307 tx_args = tx_args.with_auth_args(auth_argument);
308 }
309
310 tx_args
311 .extend_output_note_recipients(expected_output_recipients.into_values().map(Box::new));
312 tx_args.extend_merkle_store(merkle_store.inner_nodes());
313
314 tx_args
315 }
316
317 pub(crate) fn build_transaction_script(
320 &self,
321 account_interface: &AccountInterface,
322 in_debug_mode: DebugMode,
323 ) -> Result<TransactionScript, TransactionRequestError> {
324 match &self.script_template {
325 Some(TransactionScriptTemplate::CustomScript(script)) => Ok(script.clone()),
326 Some(TransactionScriptTemplate::SendNotes(notes)) => Ok(account_interface
327 .build_send_notes_script(notes, self.expiration_delta, in_debug_mode.into())?),
328 None => {
329 let empty_script = ScriptBuilder::new(true).compile_tx_script("begin nop end")?;
330
331 Ok(empty_script)
332 },
333 }
334 }
335}
336
337impl Serializable for TransactionRequest {
341 fn write_into<W: ByteWriter>(&self, target: &mut W) {
342 self.unauthenticated_input_notes.write_into(target);
343 self.input_notes.write_into(target);
344 match &self.script_template {
345 None => target.write_u8(0),
346 Some(TransactionScriptTemplate::CustomScript(script)) => {
347 target.write_u8(1);
348 script.write_into(target);
349 },
350 Some(TransactionScriptTemplate::SendNotes(notes)) => {
351 target.write_u8(2);
352 notes.write_into(target);
353 },
354 }
355 self.expected_output_recipients.write_into(target);
356 self.expected_future_notes.write_into(target);
357 self.advice_map.write_into(target);
358 self.merkle_store.write_into(target);
359 self.foreign_accounts.write_into(target);
360 self.expiration_delta.write_into(target);
361 target.write_u8(u8::from(self.ignore_invalid_input_notes));
362 self.script_arg.write_into(target);
363 self.auth_arg.write_into(target);
364 }
365}
366
367impl Deserializable for TransactionRequest {
368 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
369 let unauthenticated_input_notes = Vec::<Note>::read_from(source)?;
370 let input_notes = Vec::<(NoteId, Option<NoteArgs>)>::read_from(source)?;
371
372 let script_template = match source.read_u8()? {
373 0 => None,
374 1 => {
375 let transaction_script = TransactionScript::read_from(source)?;
376 Some(TransactionScriptTemplate::CustomScript(transaction_script))
377 },
378 2 => {
379 let notes = Vec::<PartialNote>::read_from(source)?;
380 Some(TransactionScriptTemplate::SendNotes(notes))
381 },
382 _ => {
383 return Err(DeserializationError::InvalidValue(
384 "Invalid script template type".to_string(),
385 ));
386 },
387 };
388
389 let expected_output_recipients = BTreeMap::<Word, NoteRecipient>::read_from(source)?;
390 let expected_future_notes = BTreeMap::<NoteId, (NoteDetails, NoteTag)>::read_from(source)?;
391
392 let advice_map = AdviceMap::read_from(source)?;
393 let merkle_store = MerkleStore::read_from(source)?;
394 let foreign_accounts = BTreeSet::<ForeignAccount>::read_from(source)?;
395 let expiration_delta = Option::<u16>::read_from(source)?;
396 let ignore_invalid_input_notes = source.read_u8()? == 1;
397 let script_arg = Option::<Word>::read_from(source)?;
398 let auth_arg = Option::<Word>::read_from(source)?;
399
400 Ok(TransactionRequest {
401 unauthenticated_input_notes,
402 input_notes,
403 script_template,
404 expected_output_recipients,
405 expected_future_notes,
406 advice_map,
407 merkle_store,
408 foreign_accounts,
409 expiration_delta,
410 ignore_invalid_input_notes,
411 script_arg,
412 auth_arg,
413 })
414 }
415}
416
417impl Default for TransactionRequestBuilder {
418 fn default() -> Self {
419 Self::new()
420 }
421}
422
423#[derive(Debug, Error)]
428pub enum TransactionRequestError {
429 #[error("account interface error")]
430 AccountInterfaceError(#[from] AccountInterfaceError),
431 #[error("account error")]
432 AccountError(#[from] AccountError),
433 #[error("duplicate input note with IDs: {0}")]
434 DuplicateInputNote(NoteId),
435 #[error("foreign account data missing in the account proof")]
436 ForeignAccountDataMissing,
437 #[error("foreign account storage slot {0} is not a map type")]
438 ForeignAccountStorageSlotInvalidIndex(u8),
439 #[error("requested foreign account with ID {0} does not have an expected storage mode")]
440 InvalidForeignAccountId(AccountId),
441 #[error("note {0} does not contain a valid inclusion proof")]
442 InputNoteNotAuthenticated(NoteId),
443 #[error("note {0} has already been consumed")]
444 InputNoteAlreadyConsumed(NoteId),
445 #[error("own notes shouldn't be of the header variant")]
446 InvalidNoteVariant,
447 #[error("invalid sender account id: {0}")]
448 InvalidSenderAccount(AccountId),
449 #[error("invalid transaction script")]
450 InvalidTransactionScript(#[from] TransactionScriptError),
451 #[error("merkle error")]
452 MerkleError(#[from] MerkleError),
453 #[error("specified authenticated input note with id {0} is missing")]
454 MissingAuthenticatedInputNote(NoteId),
455 #[error("a transaction without output notes must have at least one input note")]
456 NoInputNotes,
457 #[error("note not found: {0}")]
458 NoteNotFound(String),
459 #[error("note creation error")]
460 NoteCreationError(#[from] NoteError),
461 #[error("pay to id note doesn't contain at least one asset")]
462 P2IDNoteWithoutAsset,
463 #[error("error building script: {0}")]
464 ScriptBuilderError(#[from] ScriptBuilderError),
465 #[error("transaction script template error: {0}")]
466 ScriptTemplateError(String),
467 #[error("storage slot {0} not found in account ID {1}")]
468 StorageSlotNotFound(u8, AccountId),
469 #[error("error while building the input notes: {0}")]
470 TransactionInputError(#[from] TransactionInputError),
471}
472
473#[cfg(test)]
477mod tests {
478 use std::vec::Vec;
479
480 use miden_lib::account::auth::AuthRpoFalcon512;
481 use miden_lib::note::create_p2id_note;
482 use miden_lib::testing::account_component::MockAccountComponent;
483 use miden_objects::account::{AccountBuilder, AccountId, AccountType};
484 use miden_objects::asset::FungibleAsset;
485 use miden_objects::crypto::dsa::rpo_falcon512::PublicKey;
486 use miden_objects::crypto::rand::{FeltRng, RpoRandomCoin};
487 use miden_objects::note::{NoteTag, NoteType};
488 use miden_objects::testing::account_id::{
489 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
490 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
491 ACCOUNT_ID_SENDER,
492 };
493 use miden_objects::transaction::OutputNote;
494 use miden_objects::{EMPTY_WORD, Felt, Word, ZERO};
495 use miden_tx::utils::{Deserializable, Serializable};
496
497 use super::{TransactionRequest, TransactionRequestBuilder};
498 use crate::rpc::domain::account::AccountStorageRequirements;
499 use crate::transaction::ForeignAccount;
500
501 #[test]
502 fn transaction_request_serialization() {
503 let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
504 let target_id =
505 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
506 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
507 let mut rng = RpoRandomCoin::new(Word::default());
508
509 let mut notes = vec![];
510 for i in 0..6 {
511 let note = create_p2id_note(
512 sender_id,
513 target_id,
514 vec![FungibleAsset::new(faucet_id, 100 + i).unwrap().into()],
515 NoteType::Private,
516 ZERO,
517 &mut rng,
518 )
519 .unwrap();
520 notes.push(note);
521 }
522
523 let mut advice_vec: Vec<(Word, Vec<Felt>)> = vec![];
524 for i in 0..10 {
525 advice_vec.push((rng.draw_word(), vec![Felt::new(i)]));
526 }
527
528 let account = AccountBuilder::new(Default::default())
529 .with_component(MockAccountComponent::with_empty_slots())
530 .with_auth_component(AuthRpoFalcon512::new(PublicKey::new(EMPTY_WORD)))
531 .account_type(AccountType::RegularAccountImmutableCode)
532 .storage_mode(miden_objects::account::AccountStorageMode::Private)
533 .build_existing()
534 .unwrap();
535
536 let tx_request = TransactionRequestBuilder::new()
538 .authenticated_input_notes(vec![(notes.pop().unwrap().id(), None)])
539 .unauthenticated_input_notes(vec![(notes.pop().unwrap(), None)])
540 .expected_output_recipients(vec![notes.pop().unwrap().recipient().clone()])
541 .expected_future_notes(vec![(
542 notes.pop().unwrap().into(),
543 NoteTag::from_account_id(sender_id),
544 )])
545 .extend_advice_map(advice_vec)
546 .foreign_accounts([
547 ForeignAccount::public(
548 target_id,
549 AccountStorageRequirements::new([(5u8, &[Word::default()])]),
550 )
551 .unwrap(),
552 ForeignAccount::private(account).unwrap(),
553 ])
554 .own_output_notes(vec![
555 OutputNote::Full(notes.pop().unwrap()),
556 OutputNote::Partial(notes.pop().unwrap().into()),
557 ])
558 .script_arg(rng.draw_word())
559 .auth_arg(rng.draw_word())
560 .build()
561 .unwrap();
562
563 let mut buffer = Vec::new();
564 tx_request.write_into(&mut buffer);
565
566 let deserialized_tx_request = TransactionRequest::read_from_bytes(&buffer).unwrap();
567 assert_eq!(tx_request, deserialized_tx_request);
568 }
569}