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 AssetVaultError,
16 NoteError,
17 StorageMapError,
18 TransactionInputError,
19 TransactionScriptError,
20};
21use miden_protocol::note::{
22 Note,
23 NoteDetails,
24 NoteId,
25 NoteRecipient,
26 NoteScript,
27 NoteTag,
28 PartialNote,
29};
30use miden_protocol::transaction::{InputNote, InputNotes, TransactionArgs, TransactionScript};
31use miden_protocol::vm::AdviceMap;
32use miden_standards::account::interface::{AccountInterface, AccountInterfaceError};
33use miden_standards::code_builder::CodeBuilder;
34use miden_standards::errors::CodeBuilderError;
35use miden_tx::utils::serde::{
36 ByteReader,
37 ByteWriter,
38 Deserializable,
39 DeserializationError,
40 Serializable,
41};
42use thiserror::Error;
43
44mod builder;
45pub use builder::{PaymentNoteDescription, SwapTransactionData, TransactionRequestBuilder};
46
47mod foreign;
48pub use foreign::ForeignAccount;
49pub(crate) use foreign::account_proof_into_inputs;
50
51use crate::store::InputNoteRecord;
52
53pub type NoteArgs = Word;
57
58#[derive(Clone, Debug, PartialEq, Eq)]
63pub enum TransactionScriptTemplate {
64 CustomScript(TransactionScript),
66 SendNotes(Vec<PartialNote>),
72}
73
74#[derive(Clone, Debug, PartialEq, Eq)]
80pub struct TransactionRequest {
81 input_notes: Vec<Note>,
86 input_notes_args: Vec<(NoteId, Option<NoteArgs>)>,
89 script_template: Option<TransactionScriptTemplate>,
91 expected_output_recipients: BTreeMap<Word, NoteRecipient>,
93 expected_future_notes: BTreeMap<NoteId, (NoteDetails, NoteTag)>,
98 advice_map: AdviceMap,
100 merkle_store: MerkleStore,
102 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
106 expiration_delta: Option<u16>,
109 ignore_invalid_input_notes: bool,
113 script_arg: Option<Word>,
116 auth_arg: Option<Word>,
119 expected_ntx_scripts: Vec<NoteScript>,
123}
124
125impl TransactionRequest {
126 pub fn input_notes(&self) -> &[Note] {
131 &self.input_notes
132 }
133
134 pub fn input_note_ids(&self) -> impl Iterator<Item = NoteId> {
136 self.input_notes.iter().map(Note::id)
137 }
138
139 pub fn incoming_assets(&self) -> (BTreeMap<AccountId, u64>, Vec<NonFungibleAsset>) {
141 collect_assets(self.input_notes.iter().flat_map(|note| note.assets().iter()))
142 }
143
144 pub fn get_note_args(&self) -> BTreeMap<NoteId, NoteArgs> {
147 self.input_notes_args
148 .iter()
149 .filter_map(|(note, args)| args.map(|a| (*note, a)))
150 .collect()
151 }
152
153 pub fn expected_output_own_notes(&self) -> Vec<Note> {
159 match &self.script_template {
160 Some(TransactionScriptTemplate::SendNotes(notes)) => notes
161 .iter()
162 .map(|partial| {
163 Note::new(
164 partial.assets().clone(),
165 partial.metadata().clone(),
166 self.expected_output_recipients
167 .get(&partial.recipient_digest())
168 .expect("Recipient should be included if it's an own note")
169 .clone(),
170 )
171 })
172 .collect(),
173 _ => vec![],
174 }
175 }
176
177 pub fn expected_output_recipients(&self) -> impl Iterator<Item = &NoteRecipient> {
179 self.expected_output_recipients.values()
180 }
181
182 pub fn expected_future_notes(&self) -> impl Iterator<Item = &(NoteDetails, NoteTag)> {
184 self.expected_future_notes.values()
185 }
186
187 pub fn script_template(&self) -> &Option<TransactionScriptTemplate> {
189 &self.script_template
190 }
191
192 pub fn advice_map(&self) -> &AdviceMap {
194 &self.advice_map
195 }
196
197 pub fn advice_map_mut(&mut self) -> &mut AdviceMap {
199 &mut self.advice_map
200 }
201
202 pub fn merkle_store(&self) -> &MerkleStore {
204 &self.merkle_store
205 }
206
207 pub fn foreign_accounts(&self) -> &BTreeMap<AccountId, ForeignAccount> {
209 &self.foreign_accounts
210 }
211
212 pub fn ignore_invalid_input_notes(&self) -> bool {
214 self.ignore_invalid_input_notes
215 }
216
217 pub fn script_arg(&self) -> &Option<Word> {
219 &self.script_arg
220 }
221
222 pub fn auth_arg(&self) -> &Option<Word> {
224 &self.auth_arg
225 }
226
227 pub fn expected_ntx_scripts(&self) -> &[NoteScript] {
229 &self.expected_ntx_scripts
230 }
231
232 pub(crate) fn build_input_notes(
239 &self,
240 authenticated_note_records: Vec<InputNoteRecord>,
241 ) -> Result<InputNotes<InputNote>, TransactionRequestError> {
242 let mut input_notes: BTreeMap<NoteId, InputNote> = BTreeMap::new();
243
244 for authenticated_note_record in authenticated_note_records {
246 if !authenticated_note_record.is_authenticated() {
247 return Err(TransactionRequestError::InputNoteNotAuthenticated(
248 authenticated_note_record.id(),
249 ));
250 }
251
252 if authenticated_note_record.is_consumed() {
253 return Err(TransactionRequestError::InputNoteAlreadyConsumed(
254 authenticated_note_record.id(),
255 ));
256 }
257
258 let authenticated_note_id = authenticated_note_record.id();
259 input_notes.insert(
260 authenticated_note_id,
261 authenticated_note_record
262 .try_into()
263 .expect("Authenticated note record should be convertible to InputNote"),
264 );
265 }
266
267 let authenticated_note_ids: BTreeSet<NoteId> = input_notes.keys().copied().collect();
269 for note in self.input_notes().iter().filter(|n| !authenticated_note_ids.contains(&n.id()))
270 {
271 input_notes.insert(note.id(), InputNote::Unauthenticated { note: note.clone() });
272 }
273
274 Ok(InputNotes::new(
275 self.input_note_ids()
276 .map(|note_id| {
277 input_notes
278 .remove(¬e_id)
279 .expect("The input note map was checked to contain all input notes")
280 })
281 .collect(),
282 )?)
283 }
284
285 pub(crate) fn into_transaction_args(self, tx_script: TransactionScript) -> TransactionArgs {
288 let note_args = self.get_note_args();
289 let TransactionRequest {
290 expected_output_recipients,
291 advice_map,
292 merkle_store,
293 ..
294 } = self;
295
296 let mut tx_args = TransactionArgs::new(advice_map).with_note_args(note_args);
297
298 tx_args = if let Some(argument) = self.script_arg {
299 tx_args.with_tx_script_and_args(tx_script, argument)
300 } else {
301 tx_args.with_tx_script(tx_script)
302 };
303
304 if let Some(auth_argument) = self.auth_arg {
305 tx_args = tx_args.with_auth_args(auth_argument);
306 }
307
308 tx_args
309 .extend_output_note_recipients(expected_output_recipients.into_values().map(Box::new));
310 tx_args.extend_merkle_store(merkle_store.inner_nodes());
311
312 tx_args
313 }
314
315 pub(crate) fn build_transaction_script(
318 &self,
319 account_interface: &AccountInterface,
320 ) -> Result<TransactionScript, TransactionRequestError> {
321 match &self.script_template {
322 Some(TransactionScriptTemplate::CustomScript(script)) => Ok(script.clone()),
323 Some(TransactionScriptTemplate::SendNotes(notes)) => {
324 Ok(account_interface.build_send_notes_script(notes, self.expiration_delta)?)
325 },
326 None => {
327 let empty_script = CodeBuilder::new().compile_tx_script("begin nop end")?;
328
329 Ok(empty_script)
330 },
331 }
332 }
333}
334
335impl Serializable for TransactionRequest {
339 fn write_into<W: ByteWriter>(&self, target: &mut W) {
340 self.input_notes.write_into(target);
341 self.input_notes_args.write_into(target);
342 match &self.script_template {
343 None => target.write_u8(0),
344 Some(TransactionScriptTemplate::CustomScript(script)) => {
345 target.write_u8(1);
346 script.write_into(target);
347 },
348 Some(TransactionScriptTemplate::SendNotes(notes)) => {
349 target.write_u8(2);
350 notes.write_into(target);
351 },
352 }
353 self.expected_output_recipients.write_into(target);
354 self.expected_future_notes.write_into(target);
355 self.advice_map.write_into(target);
356 self.merkle_store.write_into(target);
357 let foreign_accounts: Vec<_> = self.foreign_accounts.values().cloned().collect();
358 foreign_accounts.write_into(target);
359 self.expiration_delta.write_into(target);
360 target.write_u8(u8::from(self.ignore_invalid_input_notes));
361 self.script_arg.write_into(target);
362 self.auth_arg.write_into(target);
363 self.expected_ntx_scripts.write_into(target);
364 }
365}
366
367impl Deserializable for TransactionRequest {
368 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
369 let input_notes = Vec::<Note>::read_from(source)?;
370 let input_notes_args = 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 mut foreign_accounts = BTreeMap::new();
395 for foreign_account in Vec::<ForeignAccount>::read_from(source)? {
396 foreign_accounts.entry(foreign_account.account_id()).or_insert(foreign_account);
397 }
398 let expiration_delta = Option::<u16>::read_from(source)?;
399 let ignore_invalid_input_notes = source.read_u8()? == 1;
400 let script_arg = Option::<Word>::read_from(source)?;
401 let auth_arg = Option::<Word>::read_from(source)?;
402 let expected_ntx_scripts = Vec::<NoteScript>::read_from(source)?;
403
404 Ok(TransactionRequest {
405 input_notes,
406 input_notes_args,
407 script_template,
408 expected_output_recipients,
409 expected_future_notes,
410 advice_map,
411 merkle_store,
412 foreign_accounts,
413 expiration_delta,
414 ignore_invalid_input_notes,
415 script_arg,
416 auth_arg,
417 expected_ntx_scripts,
418 })
419 }
420}
421
422pub(crate) fn collect_assets<'a>(
427 assets: impl Iterator<Item = &'a Asset>,
428) -> (BTreeMap<AccountId, u64>, Vec<NonFungibleAsset>) {
429 let mut fungible_balance_map = BTreeMap::new();
430 let mut non_fungible_set = Vec::new();
431
432 assets.for_each(|asset| match asset {
433 Asset::Fungible(fungible) => {
434 fungible_balance_map
435 .entry(fungible.faucet_id())
436 .and_modify(|balance| *balance += fungible.amount())
437 .or_insert(fungible.amount());
438 },
439 Asset::NonFungible(non_fungible) => {
440 if !non_fungible_set.contains(non_fungible) {
441 non_fungible_set.push(*non_fungible);
442 }
443 },
444 });
445
446 (fungible_balance_map, non_fungible_set)
447}
448
449impl Default for TransactionRequestBuilder {
450 fn default() -> Self {
451 Self::new()
452 }
453}
454
455#[derive(Debug, Error)]
460pub enum TransactionRequestError {
461 #[error("account interface error")]
462 AccountInterfaceError(#[from] AccountInterfaceError),
463 #[error("account error")]
464 AccountError(#[from] AccountError),
465 #[error("duplicate input note: note {0} was added more than once to the transaction")]
466 DuplicateInputNote(NoteId),
467 #[error(
468 "the account proof does not contain the required foreign account data; re-fetch the proof and retry"
469 )]
470 ForeignAccountDataMissing,
471 #[error(
472 "foreign account {0} has an incompatible storage mode; use `ForeignAccount::public()` for public accounts and `ForeignAccount::private()` for private accounts"
473 )]
474 InvalidForeignAccountId(AccountId),
475 #[error(
476 "note {0} cannot be used as an authenticated input: it does not have a valid inclusion proof"
477 )]
478 InputNoteNotAuthenticated(NoteId),
479 #[error("note {0} has already been consumed")]
480 InputNoteAlreadyConsumed(NoteId),
481 #[error("sender account {0} is not tracked by this client or does not exist")]
482 InvalidSenderAccount(AccountId),
483 #[error("invalid transaction script")]
484 InvalidTransactionScript(#[from] TransactionScriptError),
485 #[error("merkle proof error")]
486 MerkleError(#[from] MerkleError),
487 #[error("empty transaction: the request has no input notes and no account state changes")]
488 NoInputNotesNorAccountChange,
489 #[error("note not found: {0}")]
490 NoteNotFound(String),
491 #[error("failed to create note")]
492 NoteCreationError(#[from] NoteError),
493 #[error("pay-to-ID note must contain at least one asset to transfer")]
494 P2IDNoteWithoutAsset,
495 #[error("error building script")]
496 CodeBuilderError(#[from] CodeBuilderError),
497 #[error("transaction script template error: {0}")]
498 ScriptTemplateError(String),
499 #[error("storage slot {0} not found in account ID {1}")]
500 StorageSlotNotFound(u8, AccountId),
501 #[error("error while building the input notes")]
502 TransactionInputError(#[from] TransactionInputError),
503 #[error("account storage map error")]
504 StorageMapError(#[from] StorageMapError),
505 #[error("asset vault error")]
506 AssetVaultError(#[from] AssetVaultError),
507 #[error(
508 "unsupported authentication scheme ID {0}; supported schemes are: RpoFalcon512 (0) and EcdsaK256Keccak (1)"
509 )]
510 UnsupportedAuthSchemeId(u8),
511}
512
513#[cfg(test)]
517mod tests {
518 use std::vec::Vec;
519
520 use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment};
521 use miden_protocol::account::{
522 AccountBuilder,
523 AccountComponent,
524 AccountId,
525 AccountType,
526 StorageMapKey,
527 StorageSlotName,
528 };
529 use miden_protocol::asset::FungibleAsset;
530 use miden_protocol::crypto::rand::{FeltRng, RandomCoin};
531 use miden_protocol::note::{NoteAttachment, NoteTag, NoteType};
532 use miden_protocol::testing::account_id::{
533 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
534 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
535 ACCOUNT_ID_SENDER,
536 };
537 use miden_protocol::{EMPTY_WORD, Felt, Word};
538 use miden_standards::account::auth::AuthSingleSig;
539 use miden_standards::note::P2idNote;
540 use miden_standards::testing::account_component::MockAccountComponent;
541 use miden_tx::utils::serde::{Deserializable, Serializable};
542
543 use super::{TransactionRequest, TransactionRequestBuilder};
544 use crate::rpc::domain::account::AccountStorageRequirements;
545 use crate::transaction::ForeignAccount;
546
547 #[test]
548 fn transaction_request_serialization() {
549 assert_transaction_request_serialization_with(|| {
550 AuthSingleSig::new(
551 PublicKeyCommitment::from(EMPTY_WORD),
552 AuthScheme::Falcon512Poseidon2,
553 )
554 .into()
555 });
556 }
557
558 #[test]
559 fn transaction_request_serialization_ecdsa() {
560 assert_transaction_request_serialization_with(|| {
561 AuthSingleSig::new(PublicKeyCommitment::from(EMPTY_WORD), AuthScheme::EcdsaK256Keccak)
562 .into()
563 });
564 }
565
566 fn assert_transaction_request_serialization_with<F>(auth_component: F)
567 where
568 F: FnOnce() -> AccountComponent,
569 {
570 let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
571 let target_id =
572 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
573 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
574 let mut rng = RandomCoin::new(Word::default());
575
576 let mut notes = vec![];
577 for i in 0..6 {
578 let note = P2idNote::create(
579 sender_id,
580 target_id,
581 vec![FungibleAsset::new(faucet_id, 100 + i).unwrap().into()],
582 NoteType::Private,
583 NoteAttachment::default(),
584 &mut rng,
585 )
586 .unwrap();
587 notes.push(note);
588 }
589
590 let mut advice_vec: Vec<(Word, Vec<Felt>)> = vec![];
591 for i in 0..10 {
592 advice_vec.push((rng.draw_word(), vec![Felt::new(i)]));
593 }
594
595 let account = AccountBuilder::new(Default::default())
596 .with_component(MockAccountComponent::with_empty_slots())
597 .with_auth_component(auth_component())
598 .account_type(AccountType::RegularAccountImmutableCode)
599 .storage_mode(miden_protocol::account::AccountStorageMode::Private)
600 .build_existing()
601 .unwrap();
602
603 let tx_request = TransactionRequestBuilder::new()
605 .input_notes(vec![(notes.pop().unwrap(), None)])
606 .expected_output_recipients(vec![notes.pop().unwrap().recipient().clone()])
607 .expected_future_notes(vec![(
608 notes.pop().unwrap().into(),
609 NoteTag::with_account_target(sender_id),
610 )])
611 .extend_advice_map(advice_vec)
612 .foreign_accounts([
613 ForeignAccount::public(
614 target_id,
615 AccountStorageRequirements::new([(
616 StorageSlotName::new("demo::storage_slot").unwrap(),
617 &[StorageMapKey::new(Word::default())],
618 )]),
619 )
620 .unwrap(),
621 ForeignAccount::private(&account).unwrap(),
622 ])
623 .own_output_notes(vec![notes.pop().unwrap(), notes.pop().unwrap()])
624 .script_arg(rng.draw_word())
625 .auth_arg(rng.draw_word())
626 .expected_ntx_scripts(vec![notes.first().unwrap().recipient().script().clone()])
627 .build()
628 .unwrap();
629
630 let mut buffer = Vec::new();
631 tx_request.write_into(&mut buffer);
632
633 let deserialized_tx_request = TransactionRequest::read_from_bytes(&buffer).unwrap();
634 assert_eq!(tx_request, deserialized_tx_request);
635 }
636}