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 merkle_store(&self) -> &MerkleStore {
192 &self.merkle_store
193 }
194
195 pub fn foreign_accounts(&self) -> &BTreeSet<ForeignAccount> {
197 &self.foreign_accounts
198 }
199
200 pub fn ignore_invalid_input_notes(&self) -> bool {
202 self.ignore_invalid_input_notes
203 }
204
205 pub fn script_arg(&self) -> &Option<Word> {
207 &self.script_arg
208 }
209
210 pub fn auth_arg(&self) -> &Option<Word> {
212 &self.auth_arg
213 }
214
215 pub(crate) fn build_input_notes(
220 &self,
221 authenticated_note_records: Vec<InputNoteRecord>,
222 ) -> Result<InputNotes<InputNote>, TransactionRequestError> {
223 let mut input_notes: BTreeMap<NoteId, InputNote> = BTreeMap::new();
224
225 for authenticated_note_record in authenticated_note_records {
227 if !authenticated_note_record.is_authenticated() {
228 return Err(TransactionRequestError::InputNoteNotAuthenticated(
229 authenticated_note_record.id(),
230 ));
231 }
232
233 if authenticated_note_record.is_consumed() {
234 return Err(TransactionRequestError::InputNoteAlreadyConsumed(
235 authenticated_note_record.id(),
236 ));
237 }
238
239 input_notes.insert(
240 authenticated_note_record.id(),
241 authenticated_note_record
242 .try_into()
243 .expect("Authenticated note record should be convertible to InputNote"),
244 );
245 }
246
247 for id in self.authenticated_input_note_ids() {
250 if !input_notes.contains_key(&id) {
251 return Err(TransactionRequestError::MissingAuthenticatedInputNote(id));
252 }
253 }
254
255 for unauthenticated_input_notes in &self.unauthenticated_input_notes {
257 input_notes.insert(
258 unauthenticated_input_notes.id(),
259 InputNote::Unauthenticated {
260 note: unauthenticated_input_notes.clone(),
261 },
262 );
263 }
264
265 Ok(InputNotes::new(
266 self.get_input_note_ids()
267 .iter()
268 .map(|note_id| {
269 input_notes
270 .remove(note_id)
271 .expect("The input note map was checked to contain all input notes")
272 })
273 .collect(),
274 )?)
275 }
276
277 pub(crate) fn into_transaction_args(
280 self,
281 tx_script: TransactionScript,
282 foreign_account_inputs: Vec<AccountInputs>,
283 ) -> TransactionArgs {
284 let note_args = self.get_note_args();
285 let TransactionRequest {
286 expected_output_recipients,
287 advice_map,
288 merkle_store,
289 ..
290 } = self;
291
292 let mut tx_args =
293 TransactionArgs::new(advice_map, foreign_account_inputs).with_note_args(note_args);
294
295 tx_args = if let Some(argument) = self.script_arg {
296 tx_args.with_tx_script_and_args(tx_script, argument)
297 } else {
298 tx_args.with_tx_script(tx_script)
299 };
300
301 if let Some(auth_argument) = self.auth_arg {
302 tx_args = tx_args.with_auth_args(auth_argument);
303 }
304
305 tx_args
306 .extend_output_note_recipients(expected_output_recipients.into_values().map(Box::new));
307 tx_args.extend_merkle_store(merkle_store.inner_nodes());
308
309 tx_args
310 }
311
312 pub(crate) fn build_transaction_script(
315 &self,
316 account_interface: &AccountInterface,
317 in_debug_mode: DebugMode,
318 ) -> Result<TransactionScript, TransactionRequestError> {
319 match &self.script_template {
320 Some(TransactionScriptTemplate::CustomScript(script)) => Ok(script.clone()),
321 Some(TransactionScriptTemplate::SendNotes(notes)) => Ok(account_interface
322 .build_send_notes_script(notes, self.expiration_delta, in_debug_mode.into())?),
323 None => {
324 let empty_script = ScriptBuilder::new(true).compile_tx_script("begin nop end")?;
325
326 Ok(empty_script)
327 },
328 }
329 }
330}
331
332impl Serializable for TransactionRequest {
336 fn write_into<W: ByteWriter>(&self, target: &mut W) {
337 self.unauthenticated_input_notes.write_into(target);
338 self.input_notes.write_into(target);
339 match &self.script_template {
340 None => target.write_u8(0),
341 Some(TransactionScriptTemplate::CustomScript(script)) => {
342 target.write_u8(1);
343 script.write_into(target);
344 },
345 Some(TransactionScriptTemplate::SendNotes(notes)) => {
346 target.write_u8(2);
347 notes.write_into(target);
348 },
349 }
350 self.expected_output_recipients.write_into(target);
351 self.expected_future_notes.write_into(target);
352 self.advice_map.write_into(target);
353 self.merkle_store.write_into(target);
354 self.foreign_accounts.write_into(target);
355 self.expiration_delta.write_into(target);
356 target.write_u8(u8::from(self.ignore_invalid_input_notes));
357 self.script_arg.write_into(target);
358 self.auth_arg.write_into(target);
359 }
360}
361
362impl Deserializable for TransactionRequest {
363 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
364 let unauthenticated_input_notes = Vec::<Note>::read_from(source)?;
365 let input_notes = Vec::<(NoteId, Option<NoteArgs>)>::read_from(source)?;
366
367 let script_template = match source.read_u8()? {
368 0 => None,
369 1 => {
370 let transaction_script = TransactionScript::read_from(source)?;
371 Some(TransactionScriptTemplate::CustomScript(transaction_script))
372 },
373 2 => {
374 let notes = Vec::<PartialNote>::read_from(source)?;
375 Some(TransactionScriptTemplate::SendNotes(notes))
376 },
377 _ => {
378 return Err(DeserializationError::InvalidValue(
379 "Invalid script template type".to_string(),
380 ));
381 },
382 };
383
384 let expected_output_recipients = BTreeMap::<Word, NoteRecipient>::read_from(source)?;
385 let expected_future_notes = BTreeMap::<NoteId, (NoteDetails, NoteTag)>::read_from(source)?;
386
387 let advice_map = AdviceMap::read_from(source)?;
388 let merkle_store = MerkleStore::read_from(source)?;
389 let foreign_accounts = BTreeSet::<ForeignAccount>::read_from(source)?;
390 let expiration_delta = Option::<u16>::read_from(source)?;
391 let ignore_invalid_input_notes = source.read_u8()? == 1;
392 let script_arg = Option::<Word>::read_from(source)?;
393 let auth_arg = Option::<Word>::read_from(source)?;
394
395 Ok(TransactionRequest {
396 unauthenticated_input_notes,
397 input_notes,
398 script_template,
399 expected_output_recipients,
400 expected_future_notes,
401 advice_map,
402 merkle_store,
403 foreign_accounts,
404 expiration_delta,
405 ignore_invalid_input_notes,
406 script_arg,
407 auth_arg,
408 })
409 }
410}
411
412impl Default for TransactionRequestBuilder {
413 fn default() -> Self {
414 Self::new()
415 }
416}
417
418#[derive(Debug, Error)]
423pub enum TransactionRequestError {
424 #[error("account interface error")]
425 AccountInterfaceError(#[from] AccountInterfaceError),
426 #[error("account error")]
427 AccountError(#[from] AccountError),
428 #[error("duplicate input note with IDs: {0}")]
429 DuplicateInputNote(NoteId),
430 #[error("foreign account data missing in the account proof")]
431 ForeignAccountDataMissing,
432 #[error("foreign account storage slot {0} is not a map type")]
433 ForeignAccountStorageSlotInvalidIndex(u8),
434 #[error("requested foreign account with ID {0} does not have an expected storage mode")]
435 InvalidForeignAccountId(AccountId),
436 #[error("note {0} does not contain a valid inclusion proof")]
437 InputNoteNotAuthenticated(NoteId),
438 #[error("note {0} has already been consumed")]
439 InputNoteAlreadyConsumed(NoteId),
440 #[error("own notes shouldn't be of the header variant")]
441 InvalidNoteVariant,
442 #[error("invalid sender account id: {0}")]
443 InvalidSenderAccount(AccountId),
444 #[error("invalid transaction script")]
445 InvalidTransactionScript(#[from] TransactionScriptError),
446 #[error("merkle error")]
447 MerkleError(#[from] MerkleError),
448 #[error("specified authenticated input note with id {0} is missing")]
449 MissingAuthenticatedInputNote(NoteId),
450 #[error("a transaction without output notes must have at least one input note")]
451 NoInputNotes,
452 #[error("note not found: {0}")]
453 NoteNotFound(String),
454 #[error("note creation error")]
455 NoteCreationError(#[from] NoteError),
456 #[error("pay to id note doesn't contain at least one asset")]
457 P2IDNoteWithoutAsset,
458 #[error("error building script: {0}")]
459 ScriptBuilderError(#[from] ScriptBuilderError),
460 #[error("transaction script template error: {0}")]
461 ScriptTemplateError(String),
462 #[error("storage slot {0} not found in account ID {1}")]
463 StorageSlotNotFound(u8, AccountId),
464 #[error("error while building the input notes: {0}")]
465 TransactionInputError(#[from] TransactionInputError),
466}
467
468#[cfg(test)]
472mod tests {
473 use std::vec::Vec;
474
475 use miden_lib::account::auth::AuthRpoFalcon512;
476 use miden_lib::note::create_p2id_note;
477 use miden_lib::testing::account_component::MockAccountComponent;
478 use miden_objects::account::{AccountBuilder, AccountId, AccountType};
479 use miden_objects::asset::FungibleAsset;
480 use miden_objects::crypto::dsa::rpo_falcon512::PublicKey;
481 use miden_objects::crypto::rand::{FeltRng, RpoRandomCoin};
482 use miden_objects::note::{NoteTag, NoteType};
483 use miden_objects::testing::account_id::{
484 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
485 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
486 ACCOUNT_ID_SENDER,
487 };
488 use miden_objects::transaction::OutputNote;
489 use miden_objects::{EMPTY_WORD, Felt, Word, ZERO};
490 use miden_tx::utils::{Deserializable, Serializable};
491
492 use super::{TransactionRequest, TransactionRequestBuilder};
493 use crate::rpc::domain::account::AccountStorageRequirements;
494 use crate::transaction::ForeignAccount;
495
496 #[test]
497 fn transaction_request_serialization() {
498 let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
499 let target_id =
500 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
501 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
502 let mut rng = RpoRandomCoin::new(Word::default());
503
504 let mut notes = vec![];
505 for i in 0..6 {
506 let note = create_p2id_note(
507 sender_id,
508 target_id,
509 vec![FungibleAsset::new(faucet_id, 100 + i).unwrap().into()],
510 NoteType::Private,
511 ZERO,
512 &mut rng,
513 )
514 .unwrap();
515 notes.push(note);
516 }
517
518 let mut advice_vec: Vec<(Word, Vec<Felt>)> = vec![];
519 for i in 0..10 {
520 advice_vec.push((rng.draw_word(), vec![Felt::new(i)]));
521 }
522
523 let account = AccountBuilder::new(Default::default())
524 .with_component(MockAccountComponent::with_empty_slots())
525 .with_auth_component(AuthRpoFalcon512::new(PublicKey::new(EMPTY_WORD)))
526 .account_type(AccountType::RegularAccountImmutableCode)
527 .storage_mode(miden_objects::account::AccountStorageMode::Private)
528 .build_existing()
529 .unwrap();
530
531 let tx_request = TransactionRequestBuilder::new()
533 .authenticated_input_notes(vec![(notes.pop().unwrap().id(), None)])
534 .unauthenticated_input_notes(vec![(notes.pop().unwrap(), None)])
535 .expected_output_recipients(vec![notes.pop().unwrap().recipient().clone()])
536 .expected_future_notes(vec![(
537 notes.pop().unwrap().into(),
538 NoteTag::from_account_id(sender_id),
539 )])
540 .extend_advice_map(advice_vec)
541 .foreign_accounts([
542 ForeignAccount::public(
543 target_id,
544 AccountStorageRequirements::new([(5u8, &[Word::default()])]),
545 )
546 .unwrap(),
547 ForeignAccount::private(account).unwrap(),
548 ])
549 .own_output_notes(vec![
550 OutputNote::Full(notes.pop().unwrap()),
551 OutputNote::Partial(notes.pop().unwrap().into()),
552 ])
553 .script_arg(rng.draw_word())
554 .auth_arg(rng.draw_word())
555 .build()
556 .unwrap();
557
558 let mut buffer = Vec::new();
559 tx_request.write_into(&mut buffer);
560
561 let deserialized_tx_request = TransactionRequest::read_from_bytes(&buffer).unwrap();
562 assert_eq!(tx_request, deserialized_tx_request);
563 }
564}