1use alloc::boxed::Box;
4use alloc::collections::{BTreeMap, BTreeSet};
5use alloc::string::{String, ToString};
6use alloc::sync::Arc;
7use alloc::vec::Vec;
8
9use miden_protocol::Word;
10use miden_protocol::account::AccountId;
11use miden_protocol::assembly::SourceManagerSync;
12use miden_protocol::asset::{Asset, NonFungibleAsset};
13use miden_protocol::crypto::merkle::MerkleError;
14use miden_protocol::crypto::merkle::store::MerkleStore;
15use miden_protocol::errors::{
16 AccountError,
17 AssetVaultError,
18 NoteError,
19 StorageMapError,
20 TransactionInputError,
21 TransactionScriptError,
22};
23use miden_protocol::note::{
24 Note,
25 NoteDetails,
26 NoteId,
27 NoteRecipient,
28 NoteScript,
29 NoteTag,
30 PartialNote,
31};
32use miden_protocol::transaction::{InputNote, InputNotes, TransactionArgs, TransactionScript};
33use miden_protocol::vm::AdviceMap;
34use miden_standards::account::interface::{AccountInterface, AccountInterfaceError};
35use miden_standards::code_builder::CodeBuilder;
36use miden_standards::errors::CodeBuilderError;
37use miden_tx::utils::serde::{
38 ByteReader,
39 ByteWriter,
40 Deserializable,
41 DeserializationError,
42 Serializable,
43};
44use thiserror::Error;
45
46mod builder;
47pub use builder::{PaymentNoteDescription, SwapTransactionData, TransactionRequestBuilder};
48
49mod foreign;
50pub use foreign::ForeignAccount;
51pub(crate) use foreign::account_proof_into_inputs;
52
53use crate::store::InputNoteRecord;
54
55pub type NoteArgs = Word;
59
60#[derive(Clone, Debug, PartialEq, Eq)]
65pub enum TransactionScriptTemplate {
66 CustomScript(TransactionScript),
68 SendNotes(Vec<PartialNote>),
74}
75
76#[derive(Clone, Debug, PartialEq, Eq)]
82pub struct TransactionRequest {
83 input_notes: Vec<Note>,
88 input_notes_args: Vec<(NoteId, Option<NoteArgs>)>,
91 script_template: Option<TransactionScriptTemplate>,
93 expected_output_recipients: BTreeMap<Word, NoteRecipient>,
95 expected_future_notes: BTreeMap<NoteId, (NoteDetails, NoteTag)>,
100 advice_map: AdviceMap,
102 merkle_store: MerkleStore,
104 foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
108 expiration_delta: Option<u16>,
111 ignore_invalid_input_notes: bool,
115 script_arg: Option<Word>,
118 auth_arg: Option<Word>,
121 expected_ntx_scripts: Vec<NoteScript>,
125}
126
127impl TransactionRequest {
128 pub fn input_notes(&self) -> &[Note] {
133 &self.input_notes
134 }
135
136 pub fn input_note_ids(&self) -> impl Iterator<Item = NoteId> {
138 self.input_notes.iter().map(Note::id)
139 }
140
141 pub fn incoming_assets(&self) -> (BTreeMap<AccountId, u64>, Vec<NonFungibleAsset>) {
143 collect_assets(self.input_notes.iter().flat_map(|note| note.assets().iter()))
144 }
145
146 pub fn get_note_args(&self) -> BTreeMap<NoteId, NoteArgs> {
149 self.input_notes_args
150 .iter()
151 .filter_map(|(note, args)| args.map(|a| (*note, a)))
152 .collect()
153 }
154
155 pub fn expected_output_own_notes(&self) -> Vec<Note> {
161 match &self.script_template {
162 Some(TransactionScriptTemplate::SendNotes(notes)) => notes
163 .iter()
164 .map(|partial| {
165 Note::new(
166 partial.assets().clone(),
167 partial.metadata().clone(),
168 self.expected_output_recipients
169 .get(&partial.recipient_digest())
170 .expect("Recipient should be included if it's an own note")
171 .clone(),
172 )
173 })
174 .collect(),
175 _ => vec![],
176 }
177 }
178
179 pub fn expected_output_recipients(&self) -> impl Iterator<Item = &NoteRecipient> {
181 self.expected_output_recipients.values()
182 }
183
184 pub fn expected_future_notes(&self) -> impl Iterator<Item = &(NoteDetails, NoteTag)> {
186 self.expected_future_notes.values()
187 }
188
189 pub fn script_template(&self) -> &Option<TransactionScriptTemplate> {
191 &self.script_template
192 }
193
194 pub fn advice_map(&self) -> &AdviceMap {
196 &self.advice_map
197 }
198
199 pub fn advice_map_mut(&mut self) -> &mut AdviceMap {
201 &mut self.advice_map
202 }
203
204 pub fn merkle_store(&self) -> &MerkleStore {
206 &self.merkle_store
207 }
208
209 pub fn foreign_accounts(&self) -> &BTreeMap<AccountId, ForeignAccount> {
211 &self.foreign_accounts
212 }
213
214 pub fn ignore_invalid_input_notes(&self) -> bool {
216 self.ignore_invalid_input_notes
217 }
218
219 pub fn script_arg(&self) -> &Option<Word> {
221 &self.script_arg
222 }
223
224 pub fn auth_arg(&self) -> &Option<Word> {
226 &self.auth_arg
227 }
228
229 pub fn expected_ntx_scripts(&self) -> &[NoteScript] {
231 &self.expected_ntx_scripts
232 }
233
234 pub(crate) fn build_input_notes(
241 &self,
242 authenticated_note_records: Vec<InputNoteRecord>,
243 ) -> Result<InputNotes<InputNote>, TransactionRequestError> {
244 let mut input_notes: BTreeMap<NoteId, InputNote> = BTreeMap::new();
245
246 for authenticated_note_record in authenticated_note_records {
248 if !authenticated_note_record.is_authenticated() {
249 return Err(TransactionRequestError::InputNoteNotAuthenticated(
250 authenticated_note_record.id(),
251 ));
252 }
253
254 if authenticated_note_record.is_consumed() {
255 return Err(TransactionRequestError::InputNoteAlreadyConsumed(
256 authenticated_note_record.id(),
257 ));
258 }
259
260 let authenticated_note_id = authenticated_note_record.id();
261 input_notes.insert(
262 authenticated_note_id,
263 authenticated_note_record
264 .try_into()
265 .expect("Authenticated note record should be convertible to InputNote"),
266 );
267 }
268
269 let authenticated_note_ids: BTreeSet<NoteId> = input_notes.keys().copied().collect();
271 for note in self.input_notes().iter().filter(|n| !authenticated_note_ids.contains(&n.id()))
272 {
273 input_notes.insert(note.id(), InputNote::Unauthenticated { note: note.clone() });
274 }
275
276 Ok(InputNotes::new(
277 self.input_note_ids()
278 .map(|note_id| {
279 input_notes
280 .remove(¬e_id)
281 .expect("The input note map was checked to contain all input notes")
282 })
283 .collect(),
284 )?)
285 }
286
287 pub(crate) fn into_transaction_args(self, tx_script: TransactionScript) -> TransactionArgs {
290 let note_args = self.get_note_args();
291 let TransactionRequest {
292 expected_output_recipients,
293 advice_map,
294 merkle_store,
295 ..
296 } = self;
297
298 let mut tx_args = TransactionArgs::new(advice_map).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(
327 &self,
328 account_interface: &AccountInterface,
329 source_manager: Arc<dyn SourceManagerSync>,
330 ) -> Result<TransactionScript, TransactionRequestError> {
331 match &self.script_template {
332 Some(TransactionScriptTemplate::CustomScript(script)) => Ok(script.clone()),
333 Some(TransactionScriptTemplate::SendNotes(notes)) => {
334 Ok(account_interface.build_send_notes_script(notes, self.expiration_delta)?)
337 },
338 None => {
339 let empty_script = CodeBuilder::with_source_manager(source_manager)
340 .compile_tx_script("begin nop end")?;
341
342 Ok(empty_script)
343 },
344 }
345 }
346}
347
348impl Serializable for TransactionRequest {
352 fn write_into<W: ByteWriter>(&self, target: &mut W) {
353 self.input_notes.write_into(target);
354 self.input_notes_args.write_into(target);
355 match &self.script_template {
356 None => target.write_u8(0),
357 Some(TransactionScriptTemplate::CustomScript(script)) => {
358 target.write_u8(1);
359 script.write_into(target);
360 },
361 Some(TransactionScriptTemplate::SendNotes(notes)) => {
362 target.write_u8(2);
363 notes.write_into(target);
364 },
365 }
366 self.expected_output_recipients.write_into(target);
367 self.expected_future_notes.write_into(target);
368 self.advice_map.write_into(target);
369 self.merkle_store.write_into(target);
370 let foreign_accounts: Vec<_> = self.foreign_accounts.values().cloned().collect();
371 foreign_accounts.write_into(target);
372 self.expiration_delta.write_into(target);
373 target.write_u8(u8::from(self.ignore_invalid_input_notes));
374 self.script_arg.write_into(target);
375 self.auth_arg.write_into(target);
376 self.expected_ntx_scripts.write_into(target);
377 }
378}
379
380impl Deserializable for TransactionRequest {
381 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
382 let input_notes = Vec::<Note>::read_from(source)?;
383 let input_notes_args = Vec::<(NoteId, Option<NoteArgs>)>::read_from(source)?;
384
385 let script_template = match source.read_u8()? {
386 0 => None,
387 1 => {
388 let transaction_script = TransactionScript::read_from(source)?;
389 Some(TransactionScriptTemplate::CustomScript(transaction_script))
390 },
391 2 => {
392 let notes = Vec::<PartialNote>::read_from(source)?;
393 Some(TransactionScriptTemplate::SendNotes(notes))
394 },
395 _ => {
396 return Err(DeserializationError::InvalidValue(
397 "Invalid script template type".to_string(),
398 ));
399 },
400 };
401
402 let expected_output_recipients = BTreeMap::<Word, NoteRecipient>::read_from(source)?;
403 let expected_future_notes = BTreeMap::<NoteId, (NoteDetails, NoteTag)>::read_from(source)?;
404
405 let advice_map = AdviceMap::read_from(source)?;
406 let merkle_store = MerkleStore::read_from(source)?;
407 let mut foreign_accounts = BTreeMap::new();
408 for foreign_account in Vec::<ForeignAccount>::read_from(source)? {
409 foreign_accounts.entry(foreign_account.account_id()).or_insert(foreign_account);
410 }
411 let expiration_delta = Option::<u16>::read_from(source)?;
412 let ignore_invalid_input_notes = source.read_u8()? == 1;
413 let script_arg = Option::<Word>::read_from(source)?;
414 let auth_arg = Option::<Word>::read_from(source)?;
415 let expected_ntx_scripts = Vec::<NoteScript>::read_from(source)?;
416
417 Ok(TransactionRequest {
418 input_notes,
419 input_notes_args,
420 script_template,
421 expected_output_recipients,
422 expected_future_notes,
423 advice_map,
424 merkle_store,
425 foreign_accounts,
426 expiration_delta,
427 ignore_invalid_input_notes,
428 script_arg,
429 auth_arg,
430 expected_ntx_scripts,
431 })
432 }
433}
434
435pub(crate) fn collect_assets<'a>(
440 assets: impl Iterator<Item = &'a Asset>,
441) -> (BTreeMap<AccountId, u64>, Vec<NonFungibleAsset>) {
442 let mut fungible_balance_map = BTreeMap::new();
443 let mut non_fungible_set = Vec::new();
444
445 assets.for_each(|asset| match asset {
446 Asset::Fungible(fungible) => {
447 fungible_balance_map
448 .entry(fungible.faucet_id())
449 .and_modify(|balance| *balance += fungible.amount())
450 .or_insert(fungible.amount());
451 },
452 Asset::NonFungible(non_fungible) => {
453 if !non_fungible_set.contains(non_fungible) {
454 non_fungible_set.push(*non_fungible);
455 }
456 },
457 });
458
459 (fungible_balance_map, non_fungible_set)
460}
461
462impl Default for TransactionRequestBuilder {
463 fn default() -> Self {
464 Self::new()
465 }
466}
467
468#[derive(Debug, Error)]
473pub enum TransactionRequestError {
474 #[error("account interface error")]
475 AccountInterfaceError(#[from] AccountInterfaceError),
476 #[error("account error")]
477 AccountError(#[from] AccountError),
478 #[error("duplicate input note: note {0} was added more than once to the transaction")]
479 DuplicateInputNote(NoteId),
480 #[error(
481 "the account proof does not contain the required foreign account data; re-fetch the proof and retry"
482 )]
483 ForeignAccountDataMissing,
484 #[error(
485 "foreign account {0} has an incompatible storage mode; use `ForeignAccount::public()` for public accounts and `ForeignAccount::private()` for private accounts"
486 )]
487 InvalidForeignAccountId(AccountId),
488 #[error(
489 "note {0} cannot be used as an authenticated input: it does not have a valid inclusion proof"
490 )]
491 InputNoteNotAuthenticated(NoteId),
492 #[error("note {0} has already been consumed")]
493 InputNoteAlreadyConsumed(NoteId),
494 #[error("sender account {0} is not tracked by this client or does not exist")]
495 InvalidSenderAccount(AccountId),
496 #[error("invalid transaction script")]
497 InvalidTransactionScript(#[from] TransactionScriptError),
498 #[error("merkle proof error")]
499 MerkleError(#[from] MerkleError),
500 #[error("empty transaction: the request has no input notes and no account state changes")]
501 NoInputNotesNorAccountChange,
502 #[error("note not found: {0}")]
503 NoteNotFound(String),
504 #[error("failed to create note")]
505 NoteCreationError(#[from] NoteError),
506 #[error("pay-to-ID note must contain at least one asset to transfer")]
507 P2IDNoteWithoutAsset,
508 #[error("error building script")]
509 CodeBuilderError(#[from] CodeBuilderError),
510 #[error("transaction script template error: {0}")]
511 ScriptTemplateError(String),
512 #[error("storage slot {0} not found in account ID {1}")]
513 StorageSlotNotFound(u8, AccountId),
514 #[error("error while building the input notes")]
515 TransactionInputError(#[from] TransactionInputError),
516 #[error("account storage map error")]
517 StorageMapError(#[from] StorageMapError),
518 #[error("asset vault error")]
519 AssetVaultError(#[from] AssetVaultError),
520 #[error(
521 "unsupported authentication scheme ID {0}; supported schemes are: RpoFalcon512 (0) and EcdsaK256Keccak (1)"
522 )]
523 UnsupportedAuthSchemeId(u8),
524}
525
526#[cfg(test)]
530mod tests {
531 use std::vec::Vec;
532
533 use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment};
534 use miden_protocol::account::{
535 AccountBuilder,
536 AccountComponent,
537 AccountId,
538 AccountType,
539 StorageMapKey,
540 StorageSlotName,
541 };
542 use miden_protocol::asset::FungibleAsset;
543 use miden_protocol::crypto::rand::{FeltRng, RandomCoin};
544 use miden_protocol::note::{NoteAttachment, NoteTag, NoteType};
545 use miden_protocol::testing::account_id::{
546 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
547 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
548 ACCOUNT_ID_SENDER,
549 };
550 use miden_protocol::{EMPTY_WORD, Felt, Word};
551 use miden_standards::account::auth::AuthSingleSig;
552 use miden_standards::note::P2idNote;
553 use miden_standards::testing::account_component::MockAccountComponent;
554 use miden_tx::utils::serde::{Deserializable, Serializable};
555
556 use super::{TransactionRequest, TransactionRequestBuilder};
557 use crate::rpc::domain::account::AccountStorageRequirements;
558 use crate::transaction::ForeignAccount;
559
560 #[test]
561 fn transaction_request_serialization() {
562 assert_transaction_request_serialization_with(|| {
563 AuthSingleSig::new(
564 PublicKeyCommitment::from(EMPTY_WORD),
565 AuthScheme::Falcon512Poseidon2,
566 )
567 .into()
568 });
569 }
570
571 #[test]
572 fn transaction_request_serialization_ecdsa() {
573 assert_transaction_request_serialization_with(|| {
574 AuthSingleSig::new(PublicKeyCommitment::from(EMPTY_WORD), AuthScheme::EcdsaK256Keccak)
575 .into()
576 });
577 }
578
579 fn assert_transaction_request_serialization_with<F>(auth_component: F)
580 where
581 F: FnOnce() -> AccountComponent,
582 {
583 let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
584 let target_id =
585 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
586 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
587 let mut rng = RandomCoin::new(Word::default());
588
589 let mut notes = vec![];
590 for i in 0..6 {
591 let note = P2idNote::create(
592 sender_id,
593 target_id,
594 vec![FungibleAsset::new(faucet_id, 100 + i).unwrap().into()],
595 NoteType::Private,
596 NoteAttachment::default(),
597 &mut rng,
598 )
599 .unwrap();
600 notes.push(note);
601 }
602
603 let mut advice_vec: Vec<(Word, Vec<Felt>)> = vec![];
604 for i in 0..10 {
605 advice_vec.push((rng.draw_word(), vec![Felt::new(i)]));
606 }
607
608 let account = AccountBuilder::new(Default::default())
609 .with_component(MockAccountComponent::with_empty_slots())
610 .with_auth_component(auth_component())
611 .account_type(AccountType::RegularAccountImmutableCode)
612 .storage_mode(miden_protocol::account::AccountStorageMode::Private)
613 .build_existing()
614 .unwrap();
615
616 let tx_request = TransactionRequestBuilder::new()
618 .input_notes(vec![(notes.pop().unwrap(), None)])
619 .expected_output_recipients(vec![notes.pop().unwrap().recipient().clone()])
620 .expected_future_notes(vec![(
621 notes.pop().unwrap().into(),
622 NoteTag::with_account_target(sender_id),
623 )])
624 .extend_advice_map(advice_vec)
625 .foreign_accounts([
626 ForeignAccount::public(
627 target_id,
628 AccountStorageRequirements::new([(
629 StorageSlotName::new("demo::storage_slot").unwrap(),
630 &[StorageMapKey::new(Word::default())],
631 )]),
632 )
633 .unwrap(),
634 ForeignAccount::private(&account).unwrap(),
635 ])
636 .own_output_notes(vec![notes.pop().unwrap(), notes.pop().unwrap()])
637 .script_arg(rng.draw_word())
638 .auth_arg(rng.draw_word())
639 .expected_ntx_scripts(vec![notes.first().unwrap().recipient().script().clone()])
640 .build()
641 .unwrap();
642
643 let mut buffer = Vec::new();
644 tx_request.write_into(&mut buffer);
645
646 let deserialized_tx_request = TransactionRequest::read_from_bytes(&buffer).unwrap();
647 assert_eq!(tx_request, deserialized_tx_request);
648 }
649}