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::{Note, NoteDetails, NoteId, NoteRecipient, NoteTag, PartialNote};
22use miden_protocol::transaction::{InputNote, InputNotes, TransactionArgs, TransactionScript};
23use miden_protocol::vm::AdviceMap;
24use miden_standards::account::interface::{AccountInterface, AccountInterfaceError};
25use miden_standards::code_builder::CodeBuilder;
26use miden_standards::errors::CodeBuilderError;
27use miden_tx::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable};
28use thiserror::Error;
29
30mod builder;
31pub use builder::{PaymentNoteDescription, SwapTransactionData, TransactionRequestBuilder};
32
33mod foreign;
34pub use foreign::{ForeignAccount, account_proof_into_inputs};
35
36use crate::store::InputNoteRecord;
37
38pub type NoteArgs = Word;
42
43#[derive(Clone, Debug, PartialEq, Eq)]
48pub enum TransactionScriptTemplate {
49 CustomScript(TransactionScript),
51 SendNotes(Vec<PartialNote>),
57}
58
59#[derive(Clone, Debug, PartialEq, Eq)]
65pub struct TransactionRequest {
66 input_notes: Vec<Note>,
71 input_notes_args: Vec<(NoteId, Option<NoteArgs>)>,
74 script_template: Option<TransactionScriptTemplate>,
76 expected_output_recipients: BTreeMap<Word, NoteRecipient>,
78 expected_future_notes: BTreeMap<NoteId, (NoteDetails, NoteTag)>,
83 advice_map: AdviceMap,
85 merkle_store: MerkleStore,
87 foreign_accounts: BTreeSet<ForeignAccount>,
91 expiration_delta: Option<u16>,
94 ignore_invalid_input_notes: bool,
98 script_arg: Option<Word>,
101 auth_arg: Option<Word>,
104}
105
106impl TransactionRequest {
107 pub fn input_notes(&self) -> &[Note] {
112 &self.input_notes
113 }
114
115 pub fn input_note_ids(&self) -> impl Iterator<Item = NoteId> {
117 self.input_notes.iter().map(Note::id)
118 }
119
120 pub fn incoming_assets(&self) -> (BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>) {
122 collect_assets(self.input_notes.iter().flat_map(|note| note.assets().iter()))
123 }
124
125 pub fn get_note_args(&self) -> BTreeMap<NoteId, NoteArgs> {
128 self.input_notes_args
129 .iter()
130 .filter_map(|(note, args)| args.map(|a| (*note, a)))
131 .collect()
132 }
133
134 pub fn expected_output_own_notes(&self) -> Vec<Note> {
140 match &self.script_template {
141 Some(TransactionScriptTemplate::SendNotes(notes)) => notes
142 .iter()
143 .map(|partial| {
144 Note::new(
145 partial.assets().clone(),
146 partial.metadata().clone(),
147 self.expected_output_recipients
148 .get(&partial.recipient_digest())
149 .expect("Recipient should be included if it's an own note")
150 .clone(),
151 )
152 })
153 .collect(),
154 _ => vec![],
155 }
156 }
157
158 pub fn expected_output_recipients(&self) -> impl Iterator<Item = &NoteRecipient> {
160 self.expected_output_recipients.values()
161 }
162
163 pub fn expected_future_notes(&self) -> impl Iterator<Item = &(NoteDetails, NoteTag)> {
165 self.expected_future_notes.values()
166 }
167
168 pub fn script_template(&self) -> &Option<TransactionScriptTemplate> {
170 &self.script_template
171 }
172
173 pub fn advice_map(&self) -> &AdviceMap {
175 &self.advice_map
176 }
177
178 pub fn advice_map_mut(&mut self) -> &mut AdviceMap {
180 &mut self.advice_map
181 }
182
183 pub fn merkle_store(&self) -> &MerkleStore {
185 &self.merkle_store
186 }
187
188 pub fn foreign_accounts(&self) -> &BTreeSet<ForeignAccount> {
190 &self.foreign_accounts
191 }
192
193 pub fn ignore_invalid_input_notes(&self) -> bool {
195 self.ignore_invalid_input_notes
196 }
197
198 pub fn script_arg(&self) -> &Option<Word> {
200 &self.script_arg
201 }
202
203 pub fn auth_arg(&self) -> &Option<Word> {
205 &self.auth_arg
206 }
207
208 pub(crate) fn build_input_notes(
215 &self,
216 authenticated_note_records: Vec<InputNoteRecord>,
217 ) -> Result<InputNotes<InputNote>, TransactionRequestError> {
218 let mut input_notes: BTreeMap<NoteId, InputNote> = BTreeMap::new();
219
220 for authenticated_note_record in authenticated_note_records {
222 if !authenticated_note_record.is_authenticated() {
223 return Err(TransactionRequestError::InputNoteNotAuthenticated(
224 authenticated_note_record.id(),
225 ));
226 }
227
228 if authenticated_note_record.is_consumed() {
229 return Err(TransactionRequestError::InputNoteAlreadyConsumed(
230 authenticated_note_record.id(),
231 ));
232 }
233
234 let authenticated_note_id = authenticated_note_record.id();
235 input_notes.insert(
236 authenticated_note_id,
237 authenticated_note_record
238 .try_into()
239 .expect("Authenticated note record should be convertible to InputNote"),
240 );
241 }
242
243 let authenticated_note_ids: BTreeSet<NoteId> = input_notes.keys().copied().collect();
245 for note in self.input_notes().iter().filter(|n| !authenticated_note_ids.contains(&n.id()))
246 {
247 input_notes.insert(note.id(), InputNote::Unauthenticated { note: note.clone() });
248 }
249
250 Ok(InputNotes::new(
251 self.input_note_ids()
252 .map(|note_id| {
253 input_notes
254 .remove(¬e_id)
255 .expect("The input note map was checked to contain all input notes")
256 })
257 .collect(),
258 )?)
259 }
260
261 pub(crate) fn into_transaction_args(self, tx_script: TransactionScript) -> TransactionArgs {
264 let note_args = self.get_note_args();
265 let TransactionRequest {
266 expected_output_recipients,
267 advice_map,
268 merkle_store,
269 ..
270 } = self;
271
272 let mut tx_args = TransactionArgs::new(advice_map).with_note_args(note_args);
273
274 tx_args = if let Some(argument) = self.script_arg {
275 tx_args.with_tx_script_and_args(tx_script, argument)
276 } else {
277 tx_args.with_tx_script(tx_script)
278 };
279
280 if let Some(auth_argument) = self.auth_arg {
281 tx_args = tx_args.with_auth_args(auth_argument);
282 }
283
284 tx_args
285 .extend_output_note_recipients(expected_output_recipients.into_values().map(Box::new));
286 tx_args.extend_merkle_store(merkle_store.inner_nodes());
287
288 tx_args
289 }
290
291 pub(crate) fn build_transaction_script(
294 &self,
295 account_interface: &AccountInterface,
296 ) -> Result<TransactionScript, TransactionRequestError> {
297 match &self.script_template {
298 Some(TransactionScriptTemplate::CustomScript(script)) => Ok(script.clone()),
299 Some(TransactionScriptTemplate::SendNotes(notes)) => {
300 Ok(account_interface.build_send_notes_script(notes, self.expiration_delta)?)
301 },
302 None => {
303 let empty_script = CodeBuilder::new().compile_tx_script("begin nop end")?;
304
305 Ok(empty_script)
306 },
307 }
308 }
309}
310
311impl Serializable for TransactionRequest {
315 fn write_into<W: ByteWriter>(&self, target: &mut W) {
316 self.input_notes.write_into(target);
317 self.input_notes_args.write_into(target);
318 match &self.script_template {
319 None => target.write_u8(0),
320 Some(TransactionScriptTemplate::CustomScript(script)) => {
321 target.write_u8(1);
322 script.write_into(target);
323 },
324 Some(TransactionScriptTemplate::SendNotes(notes)) => {
325 target.write_u8(2);
326 notes.write_into(target);
327 },
328 }
329 self.expected_output_recipients.write_into(target);
330 self.expected_future_notes.write_into(target);
331 self.advice_map.write_into(target);
332 self.merkle_store.write_into(target);
333 self.foreign_accounts.write_into(target);
334 self.expiration_delta.write_into(target);
335 target.write_u8(u8::from(self.ignore_invalid_input_notes));
336 self.script_arg.write_into(target);
337 self.auth_arg.write_into(target);
338 }
339}
340
341impl Deserializable for TransactionRequest {
342 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
343 let input_notes = Vec::<Note>::read_from(source)?;
344 let input_notes_args = Vec::<(NoteId, Option<NoteArgs>)>::read_from(source)?;
345
346 let script_template = match source.read_u8()? {
347 0 => None,
348 1 => {
349 let transaction_script = TransactionScript::read_from(source)?;
350 Some(TransactionScriptTemplate::CustomScript(transaction_script))
351 },
352 2 => {
353 let notes = Vec::<PartialNote>::read_from(source)?;
354 Some(TransactionScriptTemplate::SendNotes(notes))
355 },
356 _ => {
357 return Err(DeserializationError::InvalidValue(
358 "Invalid script template type".to_string(),
359 ));
360 },
361 };
362
363 let expected_output_recipients = BTreeMap::<Word, NoteRecipient>::read_from(source)?;
364 let expected_future_notes = BTreeMap::<NoteId, (NoteDetails, NoteTag)>::read_from(source)?;
365
366 let advice_map = AdviceMap::read_from(source)?;
367 let merkle_store = MerkleStore::read_from(source)?;
368 let foreign_accounts = BTreeSet::<ForeignAccount>::read_from(source)?;
369 let expiration_delta = Option::<u16>::read_from(source)?;
370 let ignore_invalid_input_notes = source.read_u8()? == 1;
371 let script_arg = Option::<Word>::read_from(source)?;
372 let auth_arg = Option::<Word>::read_from(source)?;
373
374 Ok(TransactionRequest {
375 input_notes,
376 input_notes_args,
377 script_template,
378 expected_output_recipients,
379 expected_future_notes,
380 advice_map,
381 merkle_store,
382 foreign_accounts,
383 expiration_delta,
384 ignore_invalid_input_notes,
385 script_arg,
386 auth_arg,
387 })
388 }
389}
390
391pub(crate) fn collect_assets<'a>(
396 assets: impl Iterator<Item = &'a Asset>,
397) -> (BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>) {
398 let mut fungible_balance_map = BTreeMap::new();
399 let mut non_fungible_set = BTreeSet::new();
400
401 assets.for_each(|asset| match asset {
402 Asset::Fungible(fungible) => {
403 fungible_balance_map
404 .entry(fungible.faucet_id())
405 .and_modify(|balance| *balance += fungible.amount())
406 .or_insert(fungible.amount());
407 },
408 Asset::NonFungible(non_fungible) => {
409 non_fungible_set.insert(*non_fungible);
410 },
411 });
412
413 (fungible_balance_map, non_fungible_set)
414}
415
416impl Default for TransactionRequestBuilder {
417 fn default() -> Self {
418 Self::new()
419 }
420}
421
422#[derive(Debug, Error)]
427pub enum TransactionRequestError {
428 #[error("account interface error")]
429 AccountInterfaceError(#[from] AccountInterfaceError),
430 #[error("account error")]
431 AccountError(#[from] AccountError),
432 #[error("duplicate input note with IDs: {0}")]
433 DuplicateInputNote(NoteId),
434 #[error("foreign account data missing in the account proof")]
435 ForeignAccountDataMissing,
436 #[error("foreign account storage slot {0} is not a map type")]
437 ForeignAccountStorageSlotInvalidIndex(u8),
438 #[error("requested foreign account with ID {0} does not have an expected storage mode")]
439 InvalidForeignAccountId(AccountId),
440 #[error("note {0} does not contain a valid inclusion proof")]
441 InputNoteNotAuthenticated(NoteId),
442 #[error("note {0} has already been consumed")]
443 InputNoteAlreadyConsumed(NoteId),
444 #[error("own notes shouldn't be of the header variant")]
445 InvalidNoteVariant,
446 #[error("invalid sender account id: {0}")]
447 InvalidSenderAccount(AccountId),
448 #[error("invalid transaction script")]
449 InvalidTransactionScript(#[from] TransactionScriptError),
450 #[error("merkle error")]
451 MerkleError(#[from] MerkleError),
452 #[error("a transaction without output notes must have at least one input note")]
453 NoInputNotesNorAccountChange,
454 #[error("note not found: {0}")]
455 NoteNotFound(String),
456 #[error("note creation error")]
457 NoteCreationError(#[from] NoteError),
458 #[error("pay to id note doesn't contain at least one asset")]
459 P2IDNoteWithoutAsset,
460 #[error("error building script: {0}")]
461 CodeBuilderError(#[from] CodeBuilderError),
462 #[error("transaction script template error: {0}")]
463 ScriptTemplateError(String),
464 #[error("storage slot {0} not found in account ID {1}")]
465 StorageSlotNotFound(u8, AccountId),
466 #[error("error while building the input notes: {0}")]
467 TransactionInputError(#[from] TransactionInputError),
468 #[error("storage map error")]
469 StorageMapError(#[from] StorageMapError),
470 #[error("asset vault error")]
471 AssetVaultError(#[from] AssetVaultError),
472 #[error("unsupported authentication scheme ID: {0}")]
473 UnsupportedAuthSchemeId(u8),
474}
475
476#[cfg(test)]
480mod tests {
481 use std::vec::Vec;
482
483 use miden_protocol::account::auth::PublicKeyCommitment;
484 use miden_protocol::account::{
485 AccountBuilder,
486 AccountComponent,
487 AccountId,
488 AccountType,
489 StorageSlotName,
490 };
491 use miden_protocol::asset::FungibleAsset;
492 use miden_protocol::crypto::rand::{FeltRng, RpoRandomCoin};
493 use miden_protocol::note::{NoteAttachment, NoteTag, NoteType};
494 use miden_protocol::testing::account_id::{
495 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
496 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
497 ACCOUNT_ID_SENDER,
498 };
499 use miden_protocol::transaction::OutputNote;
500 use miden_protocol::{EMPTY_WORD, Felt, Word};
501 use miden_standards::account::auth::{AuthEcdsaK256Keccak, AuthFalcon512Rpo};
502 use miden_standards::note::create_p2id_note;
503 use miden_standards::testing::account_component::MockAccountComponent;
504 use miden_tx::utils::{Deserializable, Serializable};
505
506 use super::{TransactionRequest, TransactionRequestBuilder};
507 use crate::rpc::domain::account::AccountStorageRequirements;
508 use crate::transaction::ForeignAccount;
509
510 #[test]
511 fn transaction_request_serialization() {
512 assert_transaction_request_serialization_with(|| {
513 AuthFalcon512Rpo::new(PublicKeyCommitment::from(EMPTY_WORD)).into()
514 });
515 }
516
517 #[test]
518 fn transaction_request_serialization_ecdsa() {
519 assert_transaction_request_serialization_with(|| {
520 AuthEcdsaK256Keccak::new(PublicKeyCommitment::from(EMPTY_WORD)).into()
521 });
522 }
523
524 fn assert_transaction_request_serialization_with<F>(auth_component: F)
525 where
526 F: FnOnce() -> AccountComponent,
527 {
528 let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
529 let target_id =
530 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
531 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
532 let mut rng = RpoRandomCoin::new(Word::default());
533
534 let mut notes = vec![];
535 for i in 0..6 {
536 let note = create_p2id_note(
537 sender_id,
538 target_id,
539 vec![FungibleAsset::new(faucet_id, 100 + i).unwrap().into()],
540 NoteType::Private,
541 NoteAttachment::default(),
542 &mut rng,
543 )
544 .unwrap();
545 notes.push(note);
546 }
547
548 let mut advice_vec: Vec<(Word, Vec<Felt>)> = vec![];
549 for i in 0..10 {
550 advice_vec.push((rng.draw_word(), vec![Felt::new(i)]));
551 }
552
553 let account = AccountBuilder::new(Default::default())
554 .with_component(MockAccountComponent::with_empty_slots())
555 .with_auth_component(auth_component())
556 .account_type(AccountType::RegularAccountImmutableCode)
557 .storage_mode(miden_protocol::account::AccountStorageMode::Private)
558 .build_existing()
559 .unwrap();
560
561 let tx_request = TransactionRequestBuilder::new()
563 .input_notes(vec![(notes.pop().unwrap(), None)])
564 .expected_output_recipients(vec![notes.pop().unwrap().recipient().clone()])
565 .expected_future_notes(vec![(
566 notes.pop().unwrap().into(),
567 NoteTag::with_account_target(sender_id),
568 )])
569 .extend_advice_map(advice_vec)
570 .foreign_accounts([
571 ForeignAccount::public(
572 target_id,
573 AccountStorageRequirements::new([(
574 StorageSlotName::new("demo::storage_slot").unwrap(),
575 &[Word::default()],
576 )]),
577 )
578 .unwrap(),
579 ForeignAccount::private(&account).unwrap(),
580 ])
581 .own_output_notes(vec![
582 OutputNote::Full(notes.pop().unwrap()),
583 OutputNote::Partial(notes.pop().unwrap().into()),
584 ])
585 .script_arg(rng.draw_word())
586 .auth_arg(rng.draw_word())
587 .build()
588 .unwrap();
589
590 let mut buffer = Vec::new();
591 tx_request.write_into(&mut buffer);
592
593 let deserialized_tx_request = TransactionRequest::read_from_bytes(&buffer).unwrap();
594 assert_eq!(tx_request, deserialized_tx_request);
595 }
596}